From 91457972113b1288dbf2dbe0e391de8b14f9de6d Mon Sep 17 00:00:00 2001 From: Sonic Date: Wed, 22 Apr 2026 00:10:46 +0300 Subject: [PATCH 1/4] fix(loader): loader programs parity --- program_ids.go | 2 + programs/bpf-loader/loader.go | 226 ------------------ programs/loader-v2/Finalize.go | 107 +++++++++ programs/loader-v2/Finalize_test.go | 56 +++++ programs/loader-v2/Write.go | 152 +++++++++++++ programs/loader-v2/Write_test.go | 104 +++++++++ programs/loader-v2/init_test.go | 21 ++ programs/loader-v2/instructions.go | 135 +++++++++++ programs/loader-v2/instructions_test.go | 60 +++++ programs/loader-v2/testing_utils.go | 33 +++ programs/loader-v3/Close.go | 140 ++++++++++++ programs/loader-v3/DeployWithMaxDataLen.go | 222 ++++++++++++++++++ programs/loader-v3/ExtendProgram.go | 133 +++++++++++ programs/loader-v3/InitializeBuffer.go | 121 ++++++++++ programs/loader-v3/SetAuthority.go | 126 ++++++++++ programs/loader-v3/SetAuthorityChecked.go | 107 +++++++++ programs/loader-v3/Upgrade.go | 159 +++++++++++++ programs/loader-v3/Write.go | 143 ++++++++++++ programs/loader-v3/accounts_test.go | 241 ++++++++++++++++++++ programs/loader-v3/init_test.go | 21 ++ programs/loader-v3/instructions.go | 191 ++++++++++++++++ programs/loader-v3/instructions_test.go | 253 +++++++++++++++++++++ programs/loader-v3/pda.go | 40 ++++ programs/loader-v3/state.go | 187 +++++++++++++++ programs/loader-v3/state_test.go | 134 +++++++++++ programs/loader-v4/Copy.go | 159 +++++++++++++ programs/loader-v4/Deploy.go | 112 +++++++++ programs/loader-v4/Finalize.go | 108 +++++++++ programs/loader-v4/Retract.go | 95 ++++++++ programs/loader-v4/SetProgramLength.go | 152 +++++++++++++ programs/loader-v4/TransferAuthority.go | 104 +++++++++ programs/loader-v4/Write.go | 143 ++++++++++++ programs/loader-v4/accounts_test.go | 119 ++++++++++ programs/loader-v4/init_test.go | 21 ++ programs/loader-v4/instructions.go | 187 +++++++++++++++ programs/loader-v4/instructions_test.go | 154 +++++++++++++ programs/loader-v4/state.go | 83 +++++++ programs/loader-v4/state_test.go | 76 +++++++ 38 files changed, 4401 insertions(+), 226 deletions(-) delete mode 100644 programs/bpf-loader/loader.go create mode 100644 programs/loader-v2/Finalize.go create mode 100644 programs/loader-v2/Finalize_test.go create mode 100644 programs/loader-v2/Write.go create mode 100644 programs/loader-v2/Write_test.go create mode 100644 programs/loader-v2/init_test.go create mode 100644 programs/loader-v2/instructions.go create mode 100644 programs/loader-v2/instructions_test.go create mode 100644 programs/loader-v2/testing_utils.go create mode 100644 programs/loader-v3/Close.go create mode 100644 programs/loader-v3/DeployWithMaxDataLen.go create mode 100644 programs/loader-v3/ExtendProgram.go create mode 100644 programs/loader-v3/InitializeBuffer.go create mode 100644 programs/loader-v3/SetAuthority.go create mode 100644 programs/loader-v3/SetAuthorityChecked.go create mode 100644 programs/loader-v3/Upgrade.go create mode 100644 programs/loader-v3/Write.go create mode 100644 programs/loader-v3/accounts_test.go create mode 100644 programs/loader-v3/init_test.go create mode 100644 programs/loader-v3/instructions.go create mode 100644 programs/loader-v3/instructions_test.go create mode 100644 programs/loader-v3/pda.go create mode 100644 programs/loader-v3/state.go create mode 100644 programs/loader-v3/state_test.go create mode 100644 programs/loader-v4/Copy.go create mode 100644 programs/loader-v4/Deploy.go create mode 100644 programs/loader-v4/Finalize.go create mode 100644 programs/loader-v4/Retract.go create mode 100644 programs/loader-v4/SetProgramLength.go create mode 100644 programs/loader-v4/TransferAuthority.go create mode 100644 programs/loader-v4/Write.go create mode 100644 programs/loader-v4/accounts_test.go create mode 100644 programs/loader-v4/init_test.go create mode 100644 programs/loader-v4/instructions.go create mode 100644 programs/loader-v4/instructions_test.go create mode 100644 programs/loader-v4/state.go create mode 100644 programs/loader-v4/state_test.go diff --git a/program_ids.go b/program_ids.go index 77a55c9fa..476e0fa4d 100644 --- a/program_ids.go +++ b/program_ids.go @@ -32,6 +32,8 @@ var ( // Deploys, upgrades, and executes programs on the chain. BPFLoaderProgramID = MustPublicKeyFromBase58("BPFLoader2111111111111111111111111111111111") BPFLoaderUpgradeableProgramID = MustPublicKeyFromBase58("BPFLoaderUpgradeab1e11111111111111111111111") + LoaderV4ProgramID = MustPublicKeyFromBase58("LoaderV411111111111111111111111111111111111") + NativeLoaderID = MustPublicKeyFromBase58("NativeLoader1111111111111111111111111111111") // Verify secp256k1 public key recovery operations (ecrecover). Secp256k1ProgramID = MustPublicKeyFromBase58("KeccakSecp256k11111111111111111111111111111") diff --git a/programs/bpf-loader/loader.go b/programs/bpf-loader/loader.go deleted file mode 100644 index 1c2c09367..000000000 --- a/programs/bpf-loader/loader.go +++ /dev/null @@ -1,226 +0,0 @@ -package bpfloader - -import ( - "encoding/binary" - "fmt" - - "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/programs/system" - "github.com/gagliardetto/solana-go/rpc" -) - -const ( - PACKET_DATA_SIZE int = 1280 - 40 - 8 -) - -// https://github.com/solana-labs/solana/blob/v1.7.15/cli/src/program.rs#L1683 -func calculateMaxChunkSize( - createBuilder func(offset int, data []byte) *solana.TransactionBuilder, -) (size int, err error) { - transaction, err := createBuilder(0, []byte{}).Build() - if err != nil { - return - } - signatures := make( - []solana.Signature, - transaction.Message.Header.NumRequiredSignatures, - ) - transaction.Signatures = append(transaction.Signatures, signatures...) - serialized, err := transaction.MarshalBinary() - if err != nil { - return - } - size = PACKET_DATA_SIZE - len(serialized) - 1 - return -} - -// https://github.com/solana-labs/solana/blob/v1.7.15/cli/src/program.rs#L2006 -func completePartialProgramInit( - loaderId solana.PublicKey, - payerPubkey solana.PublicKey, - elfPubkey solana.PublicKey, - account *rpc.Account, - accountDataLen int, - minimumBalance uint64, - allowExcessiveBalance bool, -) (instructions []solana.Instruction, balanceNeeded uint64, err error) { - if account.Executable { - err = fmt.Errorf("buffer account is already executable") - return - } - if !account.Owner.Equals(loaderId) && - !account.Owner.Equals(solana.SystemProgramID) { - err = fmt.Errorf( - "buffer account passed is already in use by another program", - ) - return - } - if len(account.Data.GetBinary()) > 0 && - len(account.Data.GetBinary()) < accountDataLen { - err = fmt.Errorf( - "buffer account passed is not large enough, may have been for a " + - " different deploy?", - ) - return - } - - if len(account.Data.GetBinary()) == 0 && - account.Owner.Equals(solana.SystemProgramID) { - instructions = append( - instructions, - system.NewAllocateInstruction(uint64(accountDataLen), elfPubkey). - Build(), - ) - instructions = append( - instructions, - system.NewAssignInstruction(loaderId, elfPubkey).Build(), - ) - if account.Lamports < minimumBalance { - balance := minimumBalance - account.Lamports - instructions = append( - instructions, - system.NewTransferInstruction(balance, payerPubkey, elfPubkey). - Build(), - ) - balanceNeeded = balance - } else if account.Lamports > minimumBalance && - account.Owner.Equals(solana.SystemProgramID) && - !allowExcessiveBalance { - err = fmt.Errorf( - "buffer account has a balance: %v.%v; it may already be in use", - account.Lamports/solana.LAMPORTS_PER_SOL, - account.Lamports%solana.LAMPORTS_PER_SOL, - ) - return - } - } - return -} - -func load( - payerPubkey solana.PublicKey, - account *rpc.Account, - programData []byte, - bufferDataLen int, - minimumBalance uint64, - loaderId solana.PublicKey, - bufferPubkey solana.PublicKey, - allowExcessiveBalance bool, -) ( - initialBuilder *solana.TransactionBuilder, - writeBuilders []*solana.TransactionBuilder, - finalBuilder *solana.TransactionBuilder, - balanceNeeded uint64, - err error, -) { - var instructions []solana.Instruction - if account != nil { - instructions, balanceNeeded, err = completePartialProgramInit( - loaderId, - payerPubkey, - bufferPubkey, - account, - bufferDataLen, - minimumBalance, - allowExcessiveBalance, - ) - if err != nil { - return - } - } else { - instructions = append( - instructions, - system.NewCreateAccountInstruction( - minimumBalance, - uint64(bufferDataLen), - loaderId, - payerPubkey, - bufferPubkey, - ).Build(), - ) - balanceNeeded = minimumBalance - } - if len(instructions) > 0 { - initialBuilder = solana.NewTransactionBuilder().SetFeePayer(payerPubkey) - for _, instruction := range instructions { - initialBuilder = initialBuilder.AddInstruction(instruction) - } - } - - createBuilder := func(offset int, chunk []byte) *solana.TransactionBuilder { - data := make([]byte, len(chunk)+16) - binary.LittleEndian.PutUint32(data[0:], 0) - binary.LittleEndian.PutUint32(data[4:], uint32(offset)) - binary.LittleEndian.PutUint32(data[8:], uint32(len(chunk))) - binary.LittleEndian.PutUint32(data[12:], 0) - copy(data[16:], chunk) - instruction := solana.NewInstruction( - loaderId, - solana.AccountMetaSlice{ - solana.NewAccountMeta(bufferPubkey, true, true), - }, - data, - ) - return solana.NewTransactionBuilder(). - AddInstruction(instruction). - SetFeePayer(payerPubkey) - } - - chunkSize, err := calculateMaxChunkSize(createBuilder) - if err != nil { - return - } - writeBuilders = []*solana.TransactionBuilder{} - for i := 0; i < len(programData); i += chunkSize { - end := i + chunkSize - if end > len(programData) { - end = len(programData) - } - writeBuilders = append( - writeBuilders, - createBuilder(i, programData[i:end]), - ) - } - - finalBuilder = solana.NewTransactionBuilder().SetFeePayer(payerPubkey) - { - data := make([]byte, 4) - binary.LittleEndian.PutUint32(data[0:], 1) - instruction := solana.NewInstruction( - loaderId, - solana.AccountMetaSlice{ - solana.NewAccountMeta(bufferPubkey, true, true), - }, - data, - ) - finalBuilder.AddInstruction(instruction) - } - return -} - -func Deploy( - payerPubkey solana.PublicKey, - account *rpc.Account, - programData []byte, - minimumBalance uint64, - loaderId solana.PublicKey, - bufferPubkey solana.PublicKey, - allowExcessiveBalance bool, -) ( - initialBuilder *solana.TransactionBuilder, - writeBuilders []*solana.TransactionBuilder, - finalBuilder *solana.TransactionBuilder, - balanceNeeded uint64, - err error, -) { - return load( - payerPubkey, - account, - programData, - len(programData), - minimumBalance, - loaderId, - bufferPubkey, - allowExcessiveBalance, - ) -} diff --git a/programs/loader-v2/Finalize.go b/programs/loader-v2/Finalize.go new file mode 100644 index 000000000..2cd50b987 --- /dev/null +++ b/programs/loader-v2/Finalize.go @@ -0,0 +1,107 @@ +// Copyright 2021 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 loaderv2 + +import ( + "encoding/binary" + "fmt" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// Finalize an account loaded with program data for execution. +// +// Account references: +// [0] = [WRITE, SIGNER] Account to prepare for execution +// [1] = [] Rent sysvar +type Finalize struct { + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewFinalizeInstructionBuilder() *Finalize { + return &Finalize{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 2), + } +} + +func (inst *Finalize) SetAccount(account ag_solanago.PublicKey) *Finalize { + inst.AccountMetaSlice[0] = ag_solanago.Meta(account).WRITE().SIGNER() + return inst +} + +func (inst *Finalize) GetAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[0] +} + +// SetRentSysvar attaches the rent sysvar account. The helper NewFinalizeInstruction +// uses the canonical sysvar ID; this setter is exposed for callers that wish +// to override it. +func (inst *Finalize) SetRentSysvar(rent ag_solanago.PublicKey) *Finalize { + inst.AccountMetaSlice[1] = ag_solanago.Meta(rent) + return inst +} + +func (inst *Finalize) GetRentSysvar() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[1] +} + +func (inst Finalize) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint32(Instruction_Finalize, binary.LittleEndian), + }} +} + +func (inst Finalize) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *Finalize) Validate() error { + for i, acc := range inst.AccountMetaSlice { + if acc == nil { + return fmt.Errorf("ins.AccountMetaSlice[%v] is not set", i) + } + } + return nil +} + +func (inst *Finalize) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("Finalize")). + ParentFunc(func(instructionBranch ag_treeout.Branches) { + instructionBranch.Child("Accounts").ParentFunc(func(a ag_treeout.Branches) { + a.Child(ag_format.Meta("Account", inst.AccountMetaSlice[0])) + a.Child(ag_format.Meta(" Rent", inst.AccountMetaSlice[1])) + }) + }) + }) +} + +// Finalize carries no payload: the discriminant alone is the data. +func (inst Finalize) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } +func (inst *Finalize) UnmarshalWithDecoder(_ *ag_binary.Decoder) error { return nil } + +func NewFinalizeInstruction(account ag_solanago.PublicKey) *Finalize { + return NewFinalizeInstructionBuilder(). + SetAccount(account). + SetRentSysvar(ag_solanago.SysVarRentPubkey) +} diff --git a/programs/loader-v2/Finalize_test.go b/programs/loader-v2/Finalize_test.go new file mode 100644 index 000000000..3cc9d6f19 --- /dev/null +++ b/programs/loader-v2/Finalize_test.go @@ -0,0 +1,56 @@ +// Copyright 2021 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 loaderv2 + +import ( + "testing" + + ag_solanago "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" +) + +// TestFinalize_Instruction_Bincode reproduces: +// +// loader_v2_interface::finalize(&account, &program_id) +// +// which emits just the u32 LE discriminant (1). +func TestFinalize_Instruction_Bincode(t *testing.T) { + account := ag_solanago.MustPublicKeyFromBase58("7QcXLBB23bJ4q5QUXpxLkQBr37g8mNEPSSPyVvU22qUS") + + inst, err := NewFinalizeInstruction(account).ValidateAndBuild() + require.NoError(t, err) + + data, err := inst.Data() + require.NoError(t, err) + + require.Equal(t, []byte{0x01, 0x00, 0x00, 0x00}, data) +} + +// TestFinalize_Accounts mirrors the Rust helper: [target: W+S, rent: R]. +func TestFinalize_Accounts(t *testing.T) { + account := ag_solanago.MustPublicKeyFromBase58("7QcXLBB23bJ4q5QUXpxLkQBr37g8mNEPSSPyVvU22qUS") + inst := NewFinalizeInstruction(account).Build() + + accounts := inst.Accounts() + require.Len(t, accounts, 2) + + require.Equal(t, account, accounts[0].PublicKey) + require.True(t, accounts[0].IsWritable) + require.True(t, accounts[0].IsSigner) + + require.Equal(t, ag_solanago.SysVarRentPubkey, accounts[1].PublicKey) + require.False(t, accounts[1].IsWritable) + require.False(t, accounts[1].IsSigner) +} diff --git a/programs/loader-v2/Write.go b/programs/loader-v2/Write.go new file mode 100644 index 000000000..7e18a02ee --- /dev/null +++ b/programs/loader-v2/Write.go @@ -0,0 +1,152 @@ +// Copyright 2021 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 loaderv2 + +import ( + "encoding/binary" + "errors" + "fmt" + "slices" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// Write program data into an account. +// +// Account references: +// [0] = [WRITE, SIGNER] Account to write to +type Write struct { + Offset *uint32 + Bytes []byte + + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewWriteInstructionBuilder() *Write { + return &Write{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 1), + } +} + +func (inst *Write) SetOffset(offset uint32) *Write { + inst.Offset = &offset + return inst +} + +func (inst *Write) SetBytes(data []byte) *Write { + inst.Bytes = data + return inst +} + +func (inst *Write) SetAccount(account ag_solanago.PublicKey) *Write { + inst.AccountMetaSlice[0] = ag_solanago.Meta(account).WRITE().SIGNER() + return inst +} + +func (inst *Write) GetAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[0] +} + +func (inst Write) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint32(Instruction_Write, binary.LittleEndian), + }} +} + +func (inst Write) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *Write) Validate() error { + if inst.Offset == nil { + return errors.New("Offset parameter is not set") + } + for i, acc := range inst.AccountMetaSlice { + if acc == nil { + return fmt.Errorf("ins.AccountMetaSlice[%v] is not set", i) + } + } + return nil +} + +func (inst *Write) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("Write")). + ParentFunc(func(instructionBranch ag_treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(p ag_treeout.Branches) { + p.Child(ag_format.Param("Offset", *inst.Offset)) + p.Child(ag_format.Param(" Bytes", fmt.Sprintf("%d bytes", len(inst.Bytes)))) + }) + instructionBranch.Child("Accounts").ParentFunc(func(a ag_treeout.Branches) { + a.Child(ag_format.Meta("Account", inst.AccountMetaSlice[0])) + }) + }) + }) +} + +// MarshalWithEncoder emits bincode-compatible bytes: +// +// [offset: u32 LE][len(bytes): u64 LE][bytes...] +// +// ag_binary's default slice encoder uses UVarInt for the length, which does +// not match bincode, so Vec is serialised manually. +func (inst Write) MarshalWithEncoder(encoder *ag_binary.Encoder) error { + if err := encoder.WriteUint32(*inst.Offset, binary.LittleEndian); err != nil { + return err + } + if err := encoder.WriteUint64(uint64(len(inst.Bytes)), binary.LittleEndian); err != nil { + return err + } + return encoder.WriteBytes(inst.Bytes, false) +} + +func (inst *Write) UnmarshalWithDecoder(decoder *ag_binary.Decoder) error { + offset, err := decoder.ReadUint32(binary.LittleEndian) + if err != nil { + return err + } + inst.Offset = &offset + length, err := decoder.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + bts, err := decoder.ReadNBytes(int(length)) + if err != nil { + return err + } + // Clone: ReadNBytes returns a subslice of the decoder's input, so + // retaining it here would alias whatever buffer the caller passed in. + inst.Bytes = slices.Clone(bts) + return nil +} + +func NewWriteInstruction( + offset uint32, + data []byte, + account ag_solanago.PublicKey, +) *Write { + return NewWriteInstructionBuilder(). + SetOffset(offset). + SetBytes(data). + SetAccount(account) +} diff --git a/programs/loader-v2/Write_test.go b/programs/loader-v2/Write_test.go new file mode 100644 index 000000000..d526b8072 --- /dev/null +++ b/programs/loader-v2/Write_test.go @@ -0,0 +1,104 @@ +// Copyright 2021 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 loaderv2 + +import ( + "bytes" + "testing" + + ag_solanago "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" +) + +// TestWrite_Instruction_Bincode is a golden-byte test reproducing the wire +// format produced by the upstream Rust helper +// +// loader_v2_interface::write(&account, &program_id, 42, vec![1,2,3,4,5]) +// +// via `Instruction::new_with_bincode`. Default bincode encodes: +// +// [disc: u32 LE][offset: u32 LE][len(bytes): u64 LE][bytes...] +func TestWrite_Instruction_Bincode(t *testing.T) { + account := ag_solanago.MustPublicKeyFromBase58("7QcXLBB23bJ4q5QUXpxLkQBr37g8mNEPSSPyVvU22qUS") + + inst, err := NewWriteInstruction(42, []byte{1, 2, 3, 4, 5}, account).ValidateAndBuild() + require.NoError(t, err) + + data, err := inst.Data() + require.NoError(t, err) + + expected := []byte{ + // discriminant = 0 (u32 LE) + 0x00, 0x00, 0x00, 0x00, + // offset = 42 (u32 LE) + 0x2a, 0x00, 0x00, 0x00, + // len(bytes) = 5 (u64 LE) + 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // bytes + 0x01, 0x02, 0x03, 0x04, 0x05, + } + require.Equal(t, expected, data) +} + +func TestWrite_Instruction_Bincode_EmptyBytes(t *testing.T) { + account := ag_solanago.MustPublicKeyFromBase58("7QcXLBB23bJ4q5QUXpxLkQBr37g8mNEPSSPyVvU22qUS") + + inst, err := NewWriteInstruction(0, []byte{}, account).ValidateAndBuild() + require.NoError(t, err) + + data, err := inst.Data() + require.NoError(t, err) + + expected := []byte{ + 0x00, 0x00, 0x00, 0x00, // disc = 0 + 0x00, 0x00, 0x00, 0x00, // offset = 0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // len = 0 + } + require.Equal(t, expected, data) +} + +func TestWrite_RoundTrip(t *testing.T) { + account := ag_solanago.MustPublicKeyFromBase58("7QcXLBB23bJ4q5QUXpxLkQBr37g8mNEPSSPyVvU22qUS") + + original := NewWriteInstruction(1234, []byte("hello loader"), account) + buf := new(bytes.Buffer) + require.NoError(t, encodeT(*original, buf)) + + got := new(Write) + require.NoError(t, decodeT(got, buf.Bytes())) + require.Equal(t, *original.Offset, *got.Offset) + require.Equal(t, original.Bytes, got.Bytes) +} + +// TestWrite_Accounts mirrors the Rust account-meta assertions: the target +// account is passed as [WRITE, SIGNER] and no other accounts are set. +func TestWrite_Accounts(t *testing.T) { + account := ag_solanago.MustPublicKeyFromBase58("7QcXLBB23bJ4q5QUXpxLkQBr37g8mNEPSSPyVvU22qUS") + inst := NewWriteInstruction(0, nil, account).Build() + + require.Equal(t, ProgramID, inst.ProgramID()) + accounts := inst.Accounts() + require.Len(t, accounts, 1) + require.Equal(t, account, accounts[0].PublicKey) + require.True(t, accounts[0].IsWritable) + require.True(t, accounts[0].IsSigner) +} + +func TestWrite_Validate_MissingOffset(t *testing.T) { + account := ag_solanago.MustPublicKeyFromBase58("7QcXLBB23bJ4q5QUXpxLkQBr37g8mNEPSSPyVvU22qUS") + b := NewWriteInstructionBuilder().SetAccount(account).SetBytes([]byte{1}) + _, err := b.ValidateAndBuild() + require.Error(t, err) +} diff --git a/programs/loader-v2/init_test.go b/programs/loader-v2/init_test.go new file mode 100644 index 000000000..36e3acfbf --- /dev/null +++ b/programs/loader-v2/init_test.go @@ -0,0 +1,21 @@ +// Copyright 2020 dfuse Platform Inc. +// +// 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 loaderv2 + +import "github.com/streamingfast/logging" + +func init() { + logging.TestingOverride() +} diff --git a/programs/loader-v2/instructions.go b/programs/loader-v2/instructions.go new file mode 100644 index 000000000..f13512450 --- /dev/null +++ b/programs/loader-v2/instructions.go @@ -0,0 +1,135 @@ +// Copyright 2021 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 loaderv2 implements client-side instruction builders for the +// legacy, non-upgradeable BPF loader (program ID BPFLoader2111...). +// +// As of Agave 2.2.0 the on-chain v2 processor no longer accepts management +// instructions: deploys should be performed against loader-v3 (the +// upgradeable loader) or loader-v4. This package exists for parity with the +// upstream SDK and for decoding historical transactions. For new deploys, +// use the loader-v3 or loader-v4 package instead. +package loaderv2 + +import ( + "bytes" + "encoding/binary" + "fmt" + + ag_spew "github.com/davecgh/go-spew/spew" + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_text "github.com/gagliardetto/solana-go/text" + ag_treeout "github.com/gagliardetto/treeout" +) + +// ProgramID is the default program ID this package targets: the non-deprecated +// legacy loader (BPFLoader2111...). Callers may override via SetProgramID to +// target the deprecated v1 loader (BPFLoader1111...). +var ProgramID ag_solanago.PublicKey = ag_solanago.BPFLoaderProgramID + +func SetProgramID(pubkey ag_solanago.PublicKey) error { + ProgramID = pubkey + return ag_solanago.RegisterInstructionDecoder(ProgramID, registryDecodeInstruction) +} + +const ProgramName = "BPFLoaderV2" + +func init() { + ag_solanago.MustRegisterInstructionDecoder(ProgramID, registryDecodeInstruction) +} + +const ( + Instruction_Write uint32 = iota + Instruction_Finalize +) + +func InstructionIDToName(id uint32) string { + switch id { + case Instruction_Write: + return "Write" + case Instruction_Finalize: + return "Finalize" + default: + return "" + } +} + +type Instruction struct { + ag_binary.BaseVariant +} + +func (inst *Instruction) EncodeToTree(parent ag_treeout.Branches) { + if enToTree, ok := inst.Impl.(ag_text.EncodableToTree); ok { + enToTree.EncodeToTree(parent) + } else { + parent.Child(ag_spew.Sdump(inst)) + } +} + +var InstructionImplDef = ag_binary.NewVariantDefinition( + ag_binary.Uint32TypeIDEncoding, + []ag_binary.VariantType{ + {"Write", (*Write)(nil)}, + {"Finalize", (*Finalize)(nil)}, + }, +) + +func (inst *Instruction) ProgramID() ag_solanago.PublicKey { + return ProgramID +} + +func (inst *Instruction) Accounts() (out []*ag_solanago.AccountMeta) { + return inst.Impl.(ag_solanago.AccountsGettable).GetAccounts() +} + +func (inst *Instruction) Data() ([]byte, error) { + buf := new(bytes.Buffer) + if err := ag_binary.NewBinEncoder(buf).Encode(inst); err != nil { + return nil, fmt.Errorf("unable to encode instruction: %w", err) + } + return buf.Bytes(), nil +} + +func (inst *Instruction) TextEncode(encoder *ag_text.Encoder, option *ag_text.Option) error { + return encoder.Encode(inst.Impl, option) +} + +func (inst *Instruction) UnmarshalWithDecoder(decoder *ag_binary.Decoder) error { + return inst.BaseVariant.UnmarshalBinaryVariant(decoder, InstructionImplDef) +} + +func (inst Instruction) MarshalWithEncoder(encoder *ag_binary.Encoder) error { + if err := encoder.WriteUint32(inst.TypeID.Uint32(), binary.LittleEndian); err != nil { + return fmt.Errorf("unable to write variant type: %w", err) + } + return encoder.Encode(inst.Impl) +} + +func registryDecodeInstruction(accounts []*ag_solanago.AccountMeta, data []byte) (any, error) { + return DecodeInstruction(accounts, data) +} + +func DecodeInstruction(accounts []*ag_solanago.AccountMeta, data []byte) (*Instruction, error) { + inst := new(Instruction) + if err := ag_binary.NewBinDecoder(data).Decode(inst); err != nil { + return nil, fmt.Errorf("unable to decode instruction: %w", err) + } + if v, ok := inst.Impl.(ag_solanago.AccountsSettable); ok { + if err := v.SetAccounts(accounts); err != nil { + return nil, fmt.Errorf("unable to set accounts for instruction: %w", err) + } + } + return inst, nil +} diff --git a/programs/loader-v2/instructions_test.go b/programs/loader-v2/instructions_test.go new file mode 100644 index 000000000..f6c0739f9 --- /dev/null +++ b/programs/loader-v2/instructions_test.go @@ -0,0 +1,60 @@ +// Copyright 2021 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 loaderv2 + +import ( + "testing" + + ag_solanago "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" +) + +func TestInstructionIDToName(t *testing.T) { + require.Equal(t, "Write", InstructionIDToName(Instruction_Write)) + require.Equal(t, "Finalize", InstructionIDToName(Instruction_Finalize)) + require.Equal(t, "", InstructionIDToName(99)) +} + +// TestDecodeInstruction_Write round-trips a built Write through the +// package-level DecodeInstruction, exercising the ag_binary variant +// dispatch path used by the instruction registry. +func TestDecodeInstruction_Write(t *testing.T) { + account := ag_solanago.MustPublicKeyFromBase58("7QcXLBB23bJ4q5QUXpxLkQBr37g8mNEPSSPyVvU22qUS") + inst, err := NewWriteInstruction(7, []byte("data"), account).ValidateAndBuild() + require.NoError(t, err) + + data, err := inst.Data() + require.NoError(t, err) + + decoded, err := DecodeInstruction(inst.Accounts(), data) + require.NoError(t, err) + + got, ok := decoded.Impl.(*Write) + require.True(t, ok, "expected *Write, got %T", decoded.Impl) + require.Equal(t, uint32(7), *got.Offset) + require.Equal(t, []byte("data"), got.Bytes) +} + +func TestDecodeInstruction_Finalize(t *testing.T) { + account := ag_solanago.MustPublicKeyFromBase58("7QcXLBB23bJ4q5QUXpxLkQBr37g8mNEPSSPyVvU22qUS") + inst := NewFinalizeInstruction(account).Build() + data, err := inst.Data() + require.NoError(t, err) + + decoded, err := DecodeInstruction(inst.Accounts(), data) + require.NoError(t, err) + _, ok := decoded.Impl.(*Finalize) + require.True(t, ok, "expected *Finalize, got %T", decoded.Impl) +} diff --git a/programs/loader-v2/testing_utils.go b/programs/loader-v2/testing_utils.go new file mode 100644 index 000000000..be3716f01 --- /dev/null +++ b/programs/loader-v2/testing_utils.go @@ -0,0 +1,33 @@ +// Copyright 2021 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 loaderv2 + +import ( + "bytes" + "fmt" + + ag_binary "github.com/gagliardetto/binary" +) + +func encodeT(data any, buf *bytes.Buffer) error { + if err := ag_binary.NewBinEncoder(buf).Encode(data); err != nil { + return fmt.Errorf("unable to encode instruction: %w", err) + } + return nil +} + +func decodeT(dst any, data []byte) error { + return ag_binary.NewBinDecoder(data).Decode(dst) +} diff --git a/programs/loader-v3/Close.go b/programs/loader-v3/Close.go new file mode 100644 index 000000000..fc6128b6b --- /dev/null +++ b/programs/loader-v3/Close.go @@ -0,0 +1,140 @@ +// Copyright 2021 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 loaderv3 + +import ( + "encoding/binary" + "fmt" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// Close zeroes an uninitialized, buffer, or programdata account and returns +// its lamports. +// +// Account references: +// [0] = [WRITE] Account to close +// [1] = [WRITE] Lamports recipient +// [2] = [SIGNER, optional] Authority (omit for uninitialized close) +// [3] = [WRITE, optional] Program (required when closing programdata) +// +// Tombstone (SIMD-0432): when true, the closed account is left as a +// tombstone rather than fully reclaimed, blocking future re-use of the +// address. +type Close struct { + Tombstone bool + + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewCloseInstructionBuilder() *Close { + return &Close{} +} + +func (inst *Close) SetTombstone(v bool) *Close { inst.Tombstone = v; return inst } + +func (inst Close) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint32(Instruction_Close, binary.LittleEndian), + }} +} + +func (inst Close) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *Close) Validate() error { + if len(inst.AccountMetaSlice) < 2 || len(inst.AccountMetaSlice) > 4 { + return fmt.Errorf("Close expects 2-4 accounts, got %d", len(inst.AccountMetaSlice)) + } + for i, acc := range inst.AccountMetaSlice { + if acc == nil { + return fmt.Errorf("ins.AccountMetaSlice[%v] is not set", i) + } + } + return nil +} + +func (inst *Close) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("Close")). + ParentFunc(func(ib ag_treeout.Branches) { + ib.Child("Params").ParentFunc(func(p ag_treeout.Branches) { + p.Child(ag_format.Param("Tombstone", inst.Tombstone)) + }) + ib.Child("Accounts").ParentFunc(func(a ag_treeout.Branches) { + for i, acc := range inst.AccountMetaSlice { + a.Child(ag_format.Meta(fmt.Sprintf("Account[%d]", i), acc)) + } + }) + }) + }) +} + +func (inst Close) MarshalWithEncoder(encoder *ag_binary.Encoder) error { + return encoder.WriteBool(inst.Tombstone) +} + +func (inst *Close) UnmarshalWithDecoder(decoder *ag_binary.Decoder) error { + // tombstone is an OptionalTrailingBool (SIMD-0432). + v, err := readOptionalTrailingBool(decoder, false) + if err != nil { + return err + } + inst.Tombstone = v + return nil +} + +// NewCloseInstruction is the three-account shorthand: close the target with +// the provided authority co-signing. For closing programdata (which requires +// the program account), use NewCloseAnyInstruction. +func NewCloseInstruction( + closeAddress, recipient, authority ag_solanago.PublicKey, + tombstone bool, +) *Close { + return NewCloseAnyInstruction(closeAddress, recipient, &authority, nil, tombstone) +} + +// NewCloseAnyInstruction mirrors the upstream `close_any` helper and +// accommodates the three distinct Close shapes: uninitialized (no authority, +// no program), buffer (authority only), and programdata (authority + program). +func NewCloseAnyInstruction( + closeAddress, recipient ag_solanago.PublicKey, + authority *ag_solanago.PublicKey, + program *ag_solanago.PublicKey, + tombstone bool, +) *Close { + inst := NewCloseInstructionBuilder().SetTombstone(tombstone) + metas := ag_solanago.AccountMetaSlice{ + ag_solanago.Meta(closeAddress).WRITE(), + ag_solanago.Meta(recipient).WRITE(), + } + if authority != nil { + metas = append(metas, ag_solanago.Meta(*authority).SIGNER()) + } + if program != nil { + metas = append(metas, ag_solanago.Meta(*program).WRITE()) + } + inst.AccountMetaSlice = metas + return inst +} diff --git a/programs/loader-v3/DeployWithMaxDataLen.go b/programs/loader-v3/DeployWithMaxDataLen.go new file mode 100644 index 000000000..e7ee97d99 --- /dev/null +++ b/programs/loader-v3/DeployWithMaxDataLen.go @@ -0,0 +1,222 @@ +// Copyright 2021 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 loaderv3 + +import ( + "encoding/binary" + "errors" + "fmt" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/programs/system" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// DeployWithMaxDataLen promotes a staged buffer into an executable program, +// reserving `MaxDataLen` bytes for future upgrades. +// +// Account references: +// [0] = [WRITE, SIGNER] Payer +// [1] = [WRITE] ProgramData (PDA) +// [2] = [WRITE] Program account +// [3] = [WRITE] Source buffer +// [4] = [] Rent sysvar +// [5] = [] Clock sysvar +// [6] = [] System program +// [7] = [SIGNER] Upgrade authority +// +// CloseBuffer (SIMD-0430): when true, the source buffer is closed and its +// lamports returned to the payer atomically with the deploy. +type DeployWithMaxDataLen struct { + MaxDataLen *uint64 + CloseBuffer bool + + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewDeployWithMaxDataLenInstructionBuilder() *DeployWithMaxDataLen { + return &DeployWithMaxDataLen{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 8), + } +} + +func (inst *DeployWithMaxDataLen) SetMaxDataLen(v uint64) *DeployWithMaxDataLen { + inst.MaxDataLen = &v + return inst +} + +func (inst *DeployWithMaxDataLen) SetCloseBuffer(v bool) *DeployWithMaxDataLen { + inst.CloseBuffer = v + return inst +} + +func (inst *DeployWithMaxDataLen) SetPayer(payer ag_solanago.PublicKey) *DeployWithMaxDataLen { + inst.AccountMetaSlice[0] = ag_solanago.Meta(payer).WRITE().SIGNER() + return inst +} + +func (inst *DeployWithMaxDataLen) SetProgramDataAccount(pda ag_solanago.PublicKey) *DeployWithMaxDataLen { + inst.AccountMetaSlice[1] = ag_solanago.Meta(pda).WRITE() + return inst +} + +func (inst *DeployWithMaxDataLen) SetProgramAccount(program ag_solanago.PublicKey) *DeployWithMaxDataLen { + inst.AccountMetaSlice[2] = ag_solanago.Meta(program).WRITE() + return inst +} + +func (inst *DeployWithMaxDataLen) SetBufferAccount(buffer ag_solanago.PublicKey) *DeployWithMaxDataLen { + inst.AccountMetaSlice[3] = ag_solanago.Meta(buffer).WRITE() + return inst +} + +func (inst *DeployWithMaxDataLen) SetRentSysvar(rent ag_solanago.PublicKey) *DeployWithMaxDataLen { + inst.AccountMetaSlice[4] = ag_solanago.Meta(rent) + return inst +} + +func (inst *DeployWithMaxDataLen) SetClockSysvar(clock ag_solanago.PublicKey) *DeployWithMaxDataLen { + inst.AccountMetaSlice[5] = ag_solanago.Meta(clock) + return inst +} + +func (inst *DeployWithMaxDataLen) SetSystemProgram(sys ag_solanago.PublicKey) *DeployWithMaxDataLen { + inst.AccountMetaSlice[6] = ag_solanago.Meta(sys) + return inst +} + +func (inst *DeployWithMaxDataLen) SetUpgradeAuthority(authority ag_solanago.PublicKey) *DeployWithMaxDataLen { + inst.AccountMetaSlice[7] = ag_solanago.Meta(authority).SIGNER() + return inst +} + +func (inst DeployWithMaxDataLen) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint32(Instruction_DeployWithMaxDataLen, binary.LittleEndian), + }} +} + +func (inst DeployWithMaxDataLen) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *DeployWithMaxDataLen) Validate() error { + if inst.MaxDataLen == nil { + return errors.New("MaxDataLen parameter is not set") + } + for i, acc := range inst.AccountMetaSlice { + if acc == nil { + return fmt.Errorf("ins.AccountMetaSlice[%v] is not set", i) + } + } + return nil +} + +func (inst *DeployWithMaxDataLen) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("DeployWithMaxDataLen")). + ParentFunc(func(ib ag_treeout.Branches) { + ib.Child("Params").ParentFunc(func(p ag_treeout.Branches) { + p.Child(ag_format.Param(" MaxDataLen", *inst.MaxDataLen)) + p.Child(ag_format.Param("CloseBuffer", inst.CloseBuffer)) + }) + ib.Child("Accounts").ParentFunc(func(a ag_treeout.Branches) { + a.Child(ag_format.Meta(" Payer", inst.AccountMetaSlice[0])) + a.Child(ag_format.Meta(" ProgramData", inst.AccountMetaSlice[1])) + a.Child(ag_format.Meta(" Program", inst.AccountMetaSlice[2])) + a.Child(ag_format.Meta(" Buffer", inst.AccountMetaSlice[3])) + a.Child(ag_format.Meta(" Rent", inst.AccountMetaSlice[4])) + a.Child(ag_format.Meta(" Clock", inst.AccountMetaSlice[5])) + a.Child(ag_format.Meta(" SystemProgram", inst.AccountMetaSlice[6])) + a.Child(ag_format.Meta("UpgradeAuthority", inst.AccountMetaSlice[7])) + }) + }) + }) +} + +func (inst DeployWithMaxDataLen) MarshalWithEncoder(encoder *ag_binary.Encoder) error { + if err := encoder.WriteUint64(*inst.MaxDataLen, binary.LittleEndian); err != nil { + return err + } + return encoder.WriteBool(inst.CloseBuffer) +} + +func (inst *DeployWithMaxDataLen) UnmarshalWithDecoder(decoder *ag_binary.Decoder) error { + mdl, err := decoder.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + inst.MaxDataLen = &mdl + // close_buffer is an OptionalTrailingBool (SIMD-0430). + v, err := readOptionalTrailingBool(decoder, true) + if err != nil { + return err + } + inst.CloseBuffer = v + return nil +} + +// NewDeployWithMaxDataLenInstruction builds the DeployWithMaxDataLen +// instruction alone. Use NewDeployWithMaxProgramLenInstructions for the full +// create-program-account + deploy pair that upstream's +// `deploy_with_max_program_len` returns. +func NewDeployWithMaxDataLenInstruction( + payer, programDataPDA, program, buffer, upgradeAuthority ag_solanago.PublicKey, + maxDataLen uint64, + closeBuffer bool, +) *DeployWithMaxDataLen { + return NewDeployWithMaxDataLenInstructionBuilder(). + SetMaxDataLen(maxDataLen). + SetCloseBuffer(closeBuffer). + SetPayer(payer). + SetProgramDataAccount(programDataPDA). + SetProgramAccount(program). + SetBufferAccount(buffer). + SetRentSysvar(ag_solanago.SysVarRentPubkey). + SetClockSysvar(ag_solanago.SysVarClockPubkey). + SetSystemProgram(ag_solanago.SystemProgramID). + SetUpgradeAuthority(upgradeAuthority) +} + +// NewDeployWithMaxProgramLenInstructions mirrors the upstream +// `deploy_with_max_program_len` helper: it allocates the program account via +// the system program and then performs the DeployWithMaxDataLen call. +func NewDeployWithMaxProgramLenInstructions( + payer, program, buffer, upgradeAuthority ag_solanago.PublicKey, + programLamports uint64, + maxDataLen uint64, + closeBuffer bool, +) []ag_solanago.Instruction { + programDataPDA := MustGetProgramDataAddress(program) + create := system.NewCreateAccountInstruction( + programLamports, + uint64(SizeOfProgram), + ProgramID, + payer, + program, + ).Build() + deploy := NewDeployWithMaxDataLenInstruction( + payer, programDataPDA, program, buffer, upgradeAuthority, + maxDataLen, closeBuffer, + ).Build() + return []ag_solanago.Instruction{create, deploy} +} diff --git a/programs/loader-v3/ExtendProgram.go b/programs/loader-v3/ExtendProgram.go new file mode 100644 index 000000000..2ed68bedd --- /dev/null +++ b/programs/loader-v3/ExtendProgram.go @@ -0,0 +1,133 @@ +// Copyright 2021 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 loaderv3 + +import ( + "encoding/binary" + "errors" + "fmt" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// ExtendProgram grows a program's on-chain code buffer by `AdditionalBytes` +// (SIMD-0431 caps the minimum granularity at MINIMUM_EXTEND_PROGRAM_BYTES). +// +// Account references: +// [0] = [WRITE] ProgramData (PDA) +// [1] = [WRITE] Program account +// [2] = [optional] System program (required when payer is provided) +// [3] = [WRITE, SIGNER, optional] Payer (covers any additional rent) +type ExtendProgram struct { + AdditionalBytes *uint32 + + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewExtendProgramInstructionBuilder() *ExtendProgram { + return &ExtendProgram{} +} + +func (inst *ExtendProgram) SetAdditionalBytes(v uint32) *ExtendProgram { + inst.AdditionalBytes = &v + return inst +} + +func (inst ExtendProgram) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint32(Instruction_ExtendProgram, binary.LittleEndian), + }} +} + +func (inst ExtendProgram) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *ExtendProgram) Validate() error { + if inst.AdditionalBytes == nil { + return errors.New("AdditionalBytes parameter is not set") + } + n := len(inst.AccountMetaSlice) + if n != 2 && n != 4 { + return fmt.Errorf("ExtendProgram expects 2 or 4 accounts, got %d", n) + } + for i, acc := range inst.AccountMetaSlice { + if acc == nil { + return fmt.Errorf("ins.AccountMetaSlice[%v] is not set", i) + } + } + return nil +} + +func (inst *ExtendProgram) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("ExtendProgram")). + ParentFunc(func(ib ag_treeout.Branches) { + ib.Child("Params").ParentFunc(func(p ag_treeout.Branches) { + p.Child(ag_format.Param("AdditionalBytes", *inst.AdditionalBytes)) + }) + ib.Child("Accounts").ParentFunc(func(a ag_treeout.Branches) { + for i, acc := range inst.AccountMetaSlice { + a.Child(ag_format.Meta(fmt.Sprintf("Account[%d]", i), acc)) + } + }) + }) + }) +} + +func (inst ExtendProgram) MarshalWithEncoder(encoder *ag_binary.Encoder) error { + return encoder.WriteUint32(*inst.AdditionalBytes, binary.LittleEndian) +} + +func (inst *ExtendProgram) UnmarshalWithDecoder(decoder *ag_binary.Decoder) error { + v, err := decoder.ReadUint32(binary.LittleEndian) + if err != nil { + return err + } + inst.AdditionalBytes = &v + return nil +} + +// NewExtendProgramInstruction builds the ExtendProgram instruction. Pass a +// nil `payer` for the two-account form (which requires the program to +// already hold enough lamports for the new size). +func NewExtendProgramInstruction( + program ag_solanago.PublicKey, + payer *ag_solanago.PublicKey, + additionalBytes uint32, +) *ExtendProgram { + programDataPDA := MustGetProgramDataAddress(program) + inst := NewExtendProgramInstructionBuilder().SetAdditionalBytes(additionalBytes) + metas := ag_solanago.AccountMetaSlice{ + ag_solanago.Meta(programDataPDA).WRITE(), + ag_solanago.Meta(program).WRITE(), + } + if payer != nil { + metas = append(metas, + ag_solanago.Meta(ag_solanago.SystemProgramID), + ag_solanago.Meta(*payer).WRITE().SIGNER(), + ) + } + inst.AccountMetaSlice = metas + return inst +} diff --git a/programs/loader-v3/InitializeBuffer.go b/programs/loader-v3/InitializeBuffer.go new file mode 100644 index 000000000..22d65c3a7 --- /dev/null +++ b/programs/loader-v3/InitializeBuffer.go @@ -0,0 +1,121 @@ +// Copyright 2021 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 loaderv3 + +import ( + "encoding/binary" + "fmt" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/programs/system" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// InitializeBuffer marks a freshly system-allocated account as a loader buffer +// and records the authority that is permitted to write to it. +// +// Account references: +// [0] = [WRITE] Buffer account +// [1] = [] Authority (not a signer here; only used to record the +// authority address) +type InitializeBuffer struct { + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewInitializeBufferInstructionBuilder() *InitializeBuffer { + return &InitializeBuffer{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 2), + } +} + +func (inst *InitializeBuffer) SetBufferAccount(buffer ag_solanago.PublicKey) *InitializeBuffer { + inst.AccountMetaSlice[0] = ag_solanago.Meta(buffer).WRITE() + return inst +} + +func (inst *InitializeBuffer) SetAuthority(authority ag_solanago.PublicKey) *InitializeBuffer { + inst.AccountMetaSlice[1] = ag_solanago.Meta(authority) + return inst +} + +func (inst InitializeBuffer) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint32(Instruction_InitializeBuffer, binary.LittleEndian), + }} +} + +func (inst InitializeBuffer) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *InitializeBuffer) Validate() error { + for i, acc := range inst.AccountMetaSlice { + if acc == nil { + return fmt.Errorf("ins.AccountMetaSlice[%v] is not set", i) + } + } + return nil +} + +func (inst *InitializeBuffer) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("InitializeBuffer")). + ParentFunc(func(ib ag_treeout.Branches) { + ib.Child("Accounts").ParentFunc(func(a ag_treeout.Branches) { + a.Child(ag_format.Meta(" Buffer", inst.AccountMetaSlice[0])) + a.Child(ag_format.Meta("Authority", inst.AccountMetaSlice[1])) + }) + }) + }) +} + +func (inst InitializeBuffer) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } +func (inst *InitializeBuffer) UnmarshalWithDecoder(_ *ag_binary.Decoder) error { return nil } + +// NewInitializeBufferInstruction builds the InitializeBuffer instruction +// alone (no system create). Use NewCreateBufferInstructions for the full +// create+initialize pair. +func NewInitializeBufferInstruction(buffer, authority ag_solanago.PublicKey) *InitializeBuffer { + return NewInitializeBufferInstructionBuilder(). + SetBufferAccount(buffer). + SetAuthority(authority) +} + +// NewCreateBufferInstructions mirrors the upstream `create_buffer` helper: +// it returns the pair of instructions (system CreateAccount followed by +// InitializeBuffer) needed to provision a new buffer account of the given +// program length. +func NewCreateBufferInstructions( + payer, buffer, authority ag_solanago.PublicKey, + lamports uint64, + programLen int, +) []ag_solanago.Instruction { + create := system.NewCreateAccountInstruction( + lamports, + uint64(SizeOfBuffer(programLen)), + ProgramID, + payer, + buffer, + ).Build() + initBuf := NewInitializeBufferInstruction(buffer, authority).Build() + return []ag_solanago.Instruction{create, initBuf} +} diff --git a/programs/loader-v3/SetAuthority.go b/programs/loader-v3/SetAuthority.go new file mode 100644 index 000000000..ad2e4e27b --- /dev/null +++ b/programs/loader-v3/SetAuthority.go @@ -0,0 +1,126 @@ +// Copyright 2021 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 loaderv3 + +import ( + "encoding/binary" + "fmt" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// SetAuthority changes the authority on a buffer or programdata account. +// The unchecked variant is deprecated upstream in favour of +// SetAuthorityChecked when setting a new non-nil authority, but it remains +// the only way to clear an upgrade authority (pass nil for the new authority). +// +// Account references (buffer form): +// [0] = [WRITE] Buffer account +// [1] = [SIGNER] Current authority +// [2] = [optional] New authority (omit to drop the authority) +// +// Account references (programdata form): +// [0] = [WRITE] ProgramData (PDA) +// [1] = [SIGNER] Current authority +// [2] = [optional] New authority (omit to make the program immutable) +type SetAuthority struct { + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewSetAuthorityInstructionBuilder() *SetAuthority { + return &SetAuthority{} +} + +func (inst SetAuthority) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint32(Instruction_SetAuthority, binary.LittleEndian), + }} +} + +func (inst SetAuthority) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *SetAuthority) Validate() error { + if len(inst.AccountMetaSlice) < 2 || len(inst.AccountMetaSlice) > 3 { + return fmt.Errorf("SetAuthority expects 2 or 3 accounts, got %d", len(inst.AccountMetaSlice)) + } + for i, acc := range inst.AccountMetaSlice { + if acc == nil { + return fmt.Errorf("ins.AccountMetaSlice[%v] is not set", i) + } + } + return nil +} + +func (inst *SetAuthority) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("SetAuthority")). + ParentFunc(func(ib ag_treeout.Branches) { + ib.Child("Accounts").ParentFunc(func(a ag_treeout.Branches) { + for i, acc := range inst.AccountMetaSlice { + a.Child(ag_format.Meta(fmt.Sprintf("Account[%d]", i), acc)) + } + }) + }) + }) +} + +func (inst SetAuthority) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } +func (inst *SetAuthority) UnmarshalWithDecoder(_ *ag_binary.Decoder) error { return nil } + +// NewSetBufferAuthorityInstruction builds a SetAuthority that transfers a +// buffer's authority. Upstream deprecates this in favour of +// NewSetBufferAuthorityCheckedInstruction, but it is retained for decoding +// historical transactions. +func NewSetBufferAuthorityInstruction( + buffer, currentAuthority, newAuthority ag_solanago.PublicKey, +) *SetAuthority { + inst := NewSetAuthorityInstructionBuilder() + inst.AccountMetaSlice = ag_solanago.AccountMetaSlice{ + ag_solanago.Meta(buffer).WRITE(), + ag_solanago.Meta(currentAuthority).SIGNER(), + ag_solanago.Meta(newAuthority), + } + return inst +} + +// NewSetUpgradeAuthorityInstruction builds a SetAuthority that transfers or +// drops a program's upgrade authority. Pass nil for newAuthority to make the +// program immutable. +func NewSetUpgradeAuthorityInstruction( + program, currentAuthority ag_solanago.PublicKey, + newAuthority *ag_solanago.PublicKey, +) *SetAuthority { + programDataPDA := MustGetProgramDataAddress(program) + inst := NewSetAuthorityInstructionBuilder() + metas := ag_solanago.AccountMetaSlice{ + ag_solanago.Meta(programDataPDA).WRITE(), + ag_solanago.Meta(currentAuthority).SIGNER(), + } + if newAuthority != nil { + metas = append(metas, ag_solanago.Meta(*newAuthority)) + } + inst.AccountMetaSlice = metas + return inst +} diff --git a/programs/loader-v3/SetAuthorityChecked.go b/programs/loader-v3/SetAuthorityChecked.go new file mode 100644 index 000000000..168c6059c --- /dev/null +++ b/programs/loader-v3/SetAuthorityChecked.go @@ -0,0 +1,107 @@ +// Copyright 2021 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 loaderv3 + +import ( + "encoding/binary" + "fmt" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// SetAuthorityChecked is the successor to SetAuthority: the new authority +// must co-sign the transaction, preventing typos that would lock a program. +// +// Account references: +// [0] = [WRITE] Target account (buffer or programdata) +// [1] = [SIGNER] Current authority +// [2] = [SIGNER] New authority +type SetAuthorityChecked struct { + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewSetAuthorityCheckedInstructionBuilder() *SetAuthorityChecked { + return &SetAuthorityChecked{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 3), + } +} + +func (inst SetAuthorityChecked) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint32(Instruction_SetAuthorityChecked, binary.LittleEndian), + }} +} + +func (inst SetAuthorityChecked) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *SetAuthorityChecked) Validate() error { + for i, acc := range inst.AccountMetaSlice { + if acc == nil { + return fmt.Errorf("ins.AccountMetaSlice[%v] is not set", i) + } + } + return nil +} + +func (inst *SetAuthorityChecked) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("SetAuthorityChecked")). + ParentFunc(func(ib ag_treeout.Branches) { + ib.Child("Accounts").ParentFunc(func(a ag_treeout.Branches) { + a.Child(ag_format.Meta(" Target", inst.AccountMetaSlice[0])) + a.Child(ag_format.Meta("CurrentAuthority", inst.AccountMetaSlice[1])) + a.Child(ag_format.Meta(" NewAuthority", inst.AccountMetaSlice[2])) + }) + }) + }) +} + +func (inst SetAuthorityChecked) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } +func (inst *SetAuthorityChecked) UnmarshalWithDecoder(_ *ag_binary.Decoder) error { return nil } + +// NewSetBufferAuthorityCheckedInstruction builds a SetAuthorityChecked that +// transfers a buffer's authority with new-authority co-signing. +func NewSetBufferAuthorityCheckedInstruction( + buffer, currentAuthority, newAuthority ag_solanago.PublicKey, +) *SetAuthorityChecked { + inst := NewSetAuthorityCheckedInstructionBuilder() + inst.AccountMetaSlice[0] = ag_solanago.Meta(buffer).WRITE() + inst.AccountMetaSlice[1] = ag_solanago.Meta(currentAuthority).SIGNER() + inst.AccountMetaSlice[2] = ag_solanago.Meta(newAuthority).SIGNER() + return inst +} + +// NewSetUpgradeAuthorityCheckedInstruction builds a SetAuthorityChecked that +// transfers a program's upgrade authority with new-authority co-signing. +func NewSetUpgradeAuthorityCheckedInstruction( + program, currentAuthority, newAuthority ag_solanago.PublicKey, +) *SetAuthorityChecked { + programDataPDA := MustGetProgramDataAddress(program) + inst := NewSetAuthorityCheckedInstructionBuilder() + inst.AccountMetaSlice[0] = ag_solanago.Meta(programDataPDA).WRITE() + inst.AccountMetaSlice[1] = ag_solanago.Meta(currentAuthority).SIGNER() + inst.AccountMetaSlice[2] = ag_solanago.Meta(newAuthority).SIGNER() + return inst +} diff --git a/programs/loader-v3/Upgrade.go b/programs/loader-v3/Upgrade.go new file mode 100644 index 000000000..41caec44e --- /dev/null +++ b/programs/loader-v3/Upgrade.go @@ -0,0 +1,159 @@ +// Copyright 2021 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 loaderv3 + +import ( + "encoding/binary" + "fmt" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// Upgrade replaces an existing program's code with the contents of a buffer. +// +// Account references: +// [0] = [WRITE] ProgramData (PDA) +// [1] = [WRITE] Program account +// [2] = [WRITE] Source buffer +// [3] = [WRITE] Spill (lamports recipient) +// [4] = [] Rent sysvar +// [5] = [] Clock sysvar +// [6] = [SIGNER] Upgrade authority +// +// CloseBuffer (SIMD-0430): when true, the source buffer is closed and its +// lamports sent to the spill account atomically with the upgrade. +type Upgrade struct { + CloseBuffer bool + + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewUpgradeInstructionBuilder() *Upgrade { + return &Upgrade{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 7), + } +} + +func (inst *Upgrade) SetCloseBuffer(v bool) *Upgrade { inst.CloseBuffer = v; return inst } + +func (inst *Upgrade) SetProgramDataAccount(pda ag_solanago.PublicKey) *Upgrade { + inst.AccountMetaSlice[0] = ag_solanago.Meta(pda).WRITE() + return inst +} + +func (inst *Upgrade) SetProgramAccount(program ag_solanago.PublicKey) *Upgrade { + inst.AccountMetaSlice[1] = ag_solanago.Meta(program).WRITE() + return inst +} + +func (inst *Upgrade) SetBufferAccount(buffer ag_solanago.PublicKey) *Upgrade { + inst.AccountMetaSlice[2] = ag_solanago.Meta(buffer).WRITE() + return inst +} + +func (inst *Upgrade) SetSpillAccount(spill ag_solanago.PublicKey) *Upgrade { + inst.AccountMetaSlice[3] = ag_solanago.Meta(spill).WRITE() + return inst +} + +func (inst *Upgrade) SetRentSysvar(rent ag_solanago.PublicKey) *Upgrade { + inst.AccountMetaSlice[4] = ag_solanago.Meta(rent) + return inst +} + +func (inst *Upgrade) SetClockSysvar(clock ag_solanago.PublicKey) *Upgrade { + inst.AccountMetaSlice[5] = ag_solanago.Meta(clock) + return inst +} + +func (inst *Upgrade) SetAuthority(authority ag_solanago.PublicKey) *Upgrade { + inst.AccountMetaSlice[6] = ag_solanago.Meta(authority).SIGNER() + return inst +} + +func (inst Upgrade) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint32(Instruction_Upgrade, binary.LittleEndian), + }} +} + +func (inst Upgrade) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *Upgrade) Validate() error { + for i, acc := range inst.AccountMetaSlice { + if acc == nil { + return fmt.Errorf("ins.AccountMetaSlice[%v] is not set", i) + } + } + return nil +} + +func (inst *Upgrade) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("Upgrade")). + ParentFunc(func(ib ag_treeout.Branches) { + ib.Child("Params").ParentFunc(func(p ag_treeout.Branches) { + p.Child(ag_format.Param("CloseBuffer", inst.CloseBuffer)) + }) + ib.Child("Accounts").ParentFunc(func(a ag_treeout.Branches) { + a.Child(ag_format.Meta("ProgramData", inst.AccountMetaSlice[0])) + a.Child(ag_format.Meta(" Program", inst.AccountMetaSlice[1])) + a.Child(ag_format.Meta(" Buffer", inst.AccountMetaSlice[2])) + a.Child(ag_format.Meta(" Spill", inst.AccountMetaSlice[3])) + a.Child(ag_format.Meta(" Rent", inst.AccountMetaSlice[4])) + a.Child(ag_format.Meta(" Clock", inst.AccountMetaSlice[5])) + a.Child(ag_format.Meta(" Authority", inst.AccountMetaSlice[6])) + }) + }) + }) +} + +func (inst Upgrade) MarshalWithEncoder(encoder *ag_binary.Encoder) error { + return encoder.WriteBool(inst.CloseBuffer) +} + +func (inst *Upgrade) UnmarshalWithDecoder(decoder *ag_binary.Decoder) error { + v, err := readOptionalTrailingBool(decoder, true) + if err != nil { + return err + } + inst.CloseBuffer = v + return nil +} + +func NewUpgradeInstruction( + program, buffer, authority, spill ag_solanago.PublicKey, + closeBuffer bool, +) *Upgrade { + return NewUpgradeInstructionBuilder(). + SetCloseBuffer(closeBuffer). + SetProgramDataAccount(MustGetProgramDataAddress(program)). + SetProgramAccount(program). + SetBufferAccount(buffer). + SetSpillAccount(spill). + SetRentSysvar(ag_solanago.SysVarRentPubkey). + SetClockSysvar(ag_solanago.SysVarClockPubkey). + SetAuthority(authority) +} diff --git a/programs/loader-v3/Write.go b/programs/loader-v3/Write.go new file mode 100644 index 000000000..86bee92bd --- /dev/null +++ b/programs/loader-v3/Write.go @@ -0,0 +1,143 @@ +// Copyright 2021 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 loaderv3 + +import ( + "encoding/binary" + "errors" + "fmt" + "slices" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// Write copies a chunk of program bytes into a buffer account. +// +// Account references: +// [0] = [WRITE] Buffer account to write to +// [1] = [SIGNER] Buffer authority +type Write struct { + Offset *uint32 + Bytes []byte + + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewWriteInstructionBuilder() *Write { + return &Write{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 2), + } +} + +func (inst *Write) SetOffset(offset uint32) *Write { inst.Offset = &offset; return inst } +func (inst *Write) SetBytes(data []byte) *Write { inst.Bytes = data; return inst } + +func (inst *Write) SetBufferAccount(buffer ag_solanago.PublicKey) *Write { + inst.AccountMetaSlice[0] = ag_solanago.Meta(buffer).WRITE() + return inst +} + +func (inst *Write) SetAuthority(authority ag_solanago.PublicKey) *Write { + inst.AccountMetaSlice[1] = ag_solanago.Meta(authority).SIGNER() + return inst +} + +func (inst Write) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint32(Instruction_Write, binary.LittleEndian), + }} +} + +func (inst Write) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *Write) Validate() error { + if inst.Offset == nil { + return errors.New("Offset parameter is not set") + } + for i, acc := range inst.AccountMetaSlice { + if acc == nil { + return fmt.Errorf("ins.AccountMetaSlice[%v] is not set", i) + } + } + return nil +} + +func (inst *Write) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("Write")). + ParentFunc(func(ib ag_treeout.Branches) { + ib.Child("Params").ParentFunc(func(p ag_treeout.Branches) { + p.Child(ag_format.Param("Offset", *inst.Offset)) + p.Child(ag_format.Param(" Bytes", fmt.Sprintf("%d bytes", len(inst.Bytes)))) + }) + ib.Child("Accounts").ParentFunc(func(a ag_treeout.Branches) { + a.Child(ag_format.Meta(" Buffer", inst.AccountMetaSlice[0])) + a.Child(ag_format.Meta("Authority", inst.AccountMetaSlice[1])) + }) + }) + }) +} + +func (inst Write) MarshalWithEncoder(encoder *ag_binary.Encoder) error { + if err := encoder.WriteUint32(*inst.Offset, binary.LittleEndian); err != nil { + return err + } + if err := encoder.WriteUint64(uint64(len(inst.Bytes)), binary.LittleEndian); err != nil { + return err + } + return encoder.WriteBytes(inst.Bytes, false) +} + +func (inst *Write) UnmarshalWithDecoder(decoder *ag_binary.Decoder) error { + offset, err := decoder.ReadUint32(binary.LittleEndian) + if err != nil { + return err + } + inst.Offset = &offset + length, err := decoder.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + bts, err := decoder.ReadNBytes(int(length)) + if err != nil { + return err + } + // Clone: ReadNBytes returns a subslice of the decoder's input, so + // retaining it here would alias whatever buffer the caller passed in. + inst.Bytes = slices.Clone(bts) + return nil +} + +func NewWriteInstruction( + buffer, authority ag_solanago.PublicKey, + offset uint32, + bytes []byte, +) *Write { + return NewWriteInstructionBuilder(). + SetBufferAccount(buffer). + SetAuthority(authority). + SetOffset(offset). + SetBytes(bytes) +} diff --git a/programs/loader-v3/accounts_test.go b/programs/loader-v3/accounts_test.go new file mode 100644 index 000000000..6f23bf494 --- /dev/null +++ b/programs/loader-v3/accounts_test.go @@ -0,0 +1,241 @@ +// Copyright 2021 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 loaderv3 + +import ( + "testing" + + ag_solanago "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" +) + +// Each TestAccounts_* replicates the per-instruction account-meta assertions +// from upstream's builder-fn unit tests: program ID, count, and the +// (writable, signer) flags on each account in upstream's documented order. + +func TestAccounts_InitializeBuffer(t *testing.T) { + inst := NewInitializeBufferInstruction(testBuffer, testAuthority).Build() + require.Equal(t, ProgramID, inst.ProgramID()) + a := inst.Accounts() + require.Len(t, a, 2) + require.Equal(t, testBuffer, a[0].PublicKey) + require.True(t, a[0].IsWritable) + require.False(t, a[0].IsSigner) + require.Equal(t, testAuthority, a[1].PublicKey) + require.False(t, a[1].IsWritable) + require.False(t, a[1].IsSigner) +} + +func TestAccounts_Write(t *testing.T) { + inst := NewWriteInstruction(testBuffer, testAuthority, 0, nil).Build() + a := inst.Accounts() + require.Len(t, a, 2) + require.Equal(t, testBuffer, a[0].PublicKey) + require.True(t, a[0].IsWritable) + require.False(t, a[0].IsSigner) + require.Equal(t, testAuthority, a[1].PublicKey) + require.False(t, a[1].IsWritable) + require.True(t, a[1].IsSigner) +} + +func TestAccounts_DeployWithMaxDataLen(t *testing.T) { + pda := MustGetProgramDataAddress(testProgram) + inst := NewDeployWithMaxDataLenInstruction( + testPayer, pda, testProgram, testBuffer, testAuthority, 100, true, + ).Build() + a := inst.Accounts() + require.Len(t, a, 8) + + require.Equal(t, testPayer, a[0].PublicKey) + require.True(t, a[0].IsWritable) + require.True(t, a[0].IsSigner) + + require.Equal(t, pda, a[1].PublicKey) + require.True(t, a[1].IsWritable) + require.False(t, a[1].IsSigner) + + require.Equal(t, testProgram, a[2].PublicKey) + require.True(t, a[2].IsWritable) + require.False(t, a[2].IsSigner) + + require.Equal(t, testBuffer, a[3].PublicKey) + require.True(t, a[3].IsWritable) + require.False(t, a[3].IsSigner) + + require.Equal(t, ag_solanago.SysVarRentPubkey, a[4].PublicKey) + require.False(t, a[4].IsWritable) + + require.Equal(t, ag_solanago.SysVarClockPubkey, a[5].PublicKey) + require.False(t, a[5].IsWritable) + + require.Equal(t, ag_solanago.SystemProgramID, a[6].PublicKey) + require.False(t, a[6].IsWritable) + + require.Equal(t, testAuthority, a[7].PublicKey) + require.False(t, a[7].IsWritable) + require.True(t, a[7].IsSigner) +} + +func TestAccounts_Upgrade(t *testing.T) { + pda := MustGetProgramDataAddress(testProgram) + inst := NewUpgradeInstruction(testProgram, testBuffer, testAuthority, testSpill, true).Build() + a := inst.Accounts() + require.Len(t, a, 7) + require.Equal(t, pda, a[0].PublicKey) + require.True(t, a[0].IsWritable) + require.Equal(t, testProgram, a[1].PublicKey) + require.True(t, a[1].IsWritable) + require.Equal(t, testBuffer, a[2].PublicKey) + require.True(t, a[2].IsWritable) + require.Equal(t, testSpill, a[3].PublicKey) + require.True(t, a[3].IsWritable) + require.Equal(t, ag_solanago.SysVarRentPubkey, a[4].PublicKey) + require.Equal(t, ag_solanago.SysVarClockPubkey, a[5].PublicKey) + require.Equal(t, testAuthority, a[6].PublicKey) + require.True(t, a[6].IsSigner) +} + +func TestAccounts_SetBufferAuthority(t *testing.T) { + inst := NewSetBufferAuthorityInstruction(testBuffer, testAuthority, testNewAuth).Build() + a := inst.Accounts() + require.Len(t, a, 3) + require.Equal(t, testBuffer, a[0].PublicKey) + require.True(t, a[0].IsWritable) + require.Equal(t, testAuthority, a[1].PublicKey) + require.True(t, a[1].IsSigner) + require.Equal(t, testNewAuth, a[2].PublicKey) + require.False(t, a[2].IsSigner) + require.False(t, a[2].IsWritable) +} + +func TestAccounts_SetBufferAuthorityChecked(t *testing.T) { + inst := NewSetBufferAuthorityCheckedInstruction(testBuffer, testAuthority, testNewAuth).Build() + a := inst.Accounts() + require.Len(t, a, 3) + require.Equal(t, testBuffer, a[0].PublicKey) + require.True(t, a[0].IsWritable) + require.Equal(t, testAuthority, a[1].PublicKey) + require.True(t, a[1].IsSigner) + require.Equal(t, testNewAuth, a[2].PublicKey) + require.True(t, a[2].IsSigner) + require.False(t, a[2].IsWritable) +} + +func TestAccounts_SetUpgradeAuthority_Drop(t *testing.T) { + // Passing nil for newAuthority makes the program immutable: only 2 accounts. + inst := NewSetUpgradeAuthorityInstruction(testProgram, testAuthority, nil).Build() + a := inst.Accounts() + require.Len(t, a, 2) + pda := MustGetProgramDataAddress(testProgram) + require.Equal(t, pda, a[0].PublicKey) + require.True(t, a[0].IsWritable) + require.Equal(t, testAuthority, a[1].PublicKey) + require.True(t, a[1].IsSigner) +} + +func TestAccounts_SetUpgradeAuthority_Transfer(t *testing.T) { + inst := NewSetUpgradeAuthorityInstruction(testProgram, testAuthority, &testNewAuth).Build() + a := inst.Accounts() + require.Len(t, a, 3) + require.Equal(t, testNewAuth, a[2].PublicKey) + require.False(t, a[2].IsSigner) +} + +func TestAccounts_SetUpgradeAuthorityChecked(t *testing.T) { + inst := NewSetUpgradeAuthorityCheckedInstruction(testProgram, testAuthority, testNewAuth).Build() + a := inst.Accounts() + require.Len(t, a, 3) + require.Equal(t, testNewAuth, a[2].PublicKey) + require.True(t, a[2].IsSigner) +} + +func TestAccounts_Close_Three(t *testing.T) { + inst := NewCloseInstruction(testBuffer, testPayer, testAuthority, false).Build() + a := inst.Accounts() + require.Len(t, a, 3) + require.Equal(t, testBuffer, a[0].PublicKey) + require.True(t, a[0].IsWritable) + require.Equal(t, testPayer, a[1].PublicKey) + require.True(t, a[1].IsWritable) + require.Equal(t, testAuthority, a[2].PublicKey) + require.True(t, a[2].IsSigner) +} + +func TestAccounts_CloseAny_Uninitialized(t *testing.T) { + // Uninitialized close: no authority, no program. + inst := NewCloseAnyInstruction(testBuffer, testPayer, nil, nil, false).Build() + a := inst.Accounts() + require.Len(t, a, 2) +} + +func TestAccounts_CloseAny_ProgramData(t *testing.T) { + inst := NewCloseAnyInstruction(testBuffer, testPayer, &testAuthority, &testProgram, true).Build() + a := inst.Accounts() + require.Len(t, a, 4) + require.Equal(t, testProgram, a[3].PublicKey) + require.True(t, a[3].IsWritable) +} + +func TestAccounts_ExtendProgram_NoPayer(t *testing.T) { + inst := NewExtendProgramInstruction(testProgram, nil, 10_240).Build() + a := inst.Accounts() + require.Len(t, a, 2) + pda := MustGetProgramDataAddress(testProgram) + require.Equal(t, pda, a[0].PublicKey) + require.True(t, a[0].IsWritable) + require.Equal(t, testProgram, a[1].PublicKey) + require.True(t, a[1].IsWritable) +} + +func TestAccounts_ExtendProgram_WithPayer(t *testing.T) { + inst := NewExtendProgramInstruction(testProgram, &testPayer, 10_240).Build() + a := inst.Accounts() + require.Len(t, a, 4) + require.Equal(t, ag_solanago.SystemProgramID, a[2].PublicKey) + require.False(t, a[2].IsWritable) + require.False(t, a[2].IsSigner) + require.Equal(t, testPayer, a[3].PublicKey) + require.True(t, a[3].IsWritable) + require.True(t, a[3].IsSigner) +} + +func TestCreateBufferInstructions(t *testing.T) { + insts := NewCreateBufferInstructions(testPayer, testBuffer, testAuthority, 1000, 128) + require.Len(t, insts, 2) + // ix1 is the InitializeBuffer; its program ID must be the loader. + require.Equal(t, ProgramID, insts[1].ProgramID()) +} + +func TestDeployWithMaxProgramLenInstructions(t *testing.T) { + insts := NewDeployWithMaxProgramLenInstructions( + testPayer, testProgram, testBuffer, testAuthority, + 1_000, 2_000, false, + ) + require.Len(t, insts, 2) + require.Equal(t, ProgramID, insts[1].ProgramID()) +} + +func TestPDA_GetProgramDataAddress(t *testing.T) { + // Spot-check: derivation is stable for the same inputs and distinct + // from the program address itself. + pda, bump, err := GetProgramDataAddress(testProgram) + require.NoError(t, err) + require.NotEqual(t, testProgram, pda) + require.True(t, bump <= 255) + + pda2, _, err := GetProgramDataAddress(testProgram) + require.NoError(t, err) + require.Equal(t, pda, pda2) +} diff --git a/programs/loader-v3/init_test.go b/programs/loader-v3/init_test.go new file mode 100644 index 000000000..2b50079f8 --- /dev/null +++ b/programs/loader-v3/init_test.go @@ -0,0 +1,21 @@ +// Copyright 2020 dfuse Platform Inc. +// +// 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 loaderv3 + +import "github.com/streamingfast/logging" + +func init() { + logging.TestingOverride() +} diff --git a/programs/loader-v3/instructions.go b/programs/loader-v3/instructions.go new file mode 100644 index 000000000..eb5ea5a09 --- /dev/null +++ b/programs/loader-v3/instructions.go @@ -0,0 +1,191 @@ +// Copyright 2021 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 loaderv3 implements client-side instruction builders for the +// upgradeable BPF loader (program ID BPFLoaderUpgradeab1e...), a.k.a. +// loader-v3. This is the canonical deploy/upgrade path used by +// `solana program deploy` in current Agave releases. +// +// The wire format matches upstream's default bincode encoding: u32 LE +// enum discriminant, u64 LE Vec length prefix, u8 for bool and +// Option tags, 32 raw bytes for Pubkey. The `OptionalTrailingBool` fields +// introduced by SIMD-0430 (close_buffer on DeployWithMaxDataLen/Upgrade) +// and SIMD-0432 (tombstone on Close) are always emitted on write and +// fall back to the documented defaults on an exhausted read. +package loaderv3 + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + + ag_spew "github.com/davecgh/go-spew/spew" + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_text "github.com/gagliardetto/solana-go/text" + ag_treeout "github.com/gagliardetto/treeout" +) + +// ErrInvalidBoolEncoding mirrors upstream's `InvalidBoolEncoding(byte)` error +// variant: any byte other than 0 or 1 in an OptionalTrailingBool slot is +// rejected. Callers can match with errors.Is to distinguish this from +// generic decode failures. +var ErrInvalidBoolEncoding = errors.New("invalid bool encoding") + +var ProgramID ag_solanago.PublicKey = ag_solanago.BPFLoaderUpgradeableProgramID + +func SetProgramID(pubkey ag_solanago.PublicKey) error { + ProgramID = pubkey + return ag_solanago.RegisterInstructionDecoder(ProgramID, registryDecodeInstruction) +} + +const ProgramName = "BPFLoaderUpgradeable" + +func init() { + ag_solanago.MustRegisterInstructionDecoder(ProgramID, registryDecodeInstruction) +} + +// MINIMUM_EXTEND_PROGRAM_BYTES is the minimum granularity accepted by the +// ExtendProgram instruction (SIMD-0431). +const MINIMUM_EXTEND_PROGRAM_BYTES uint32 = 10_240 + +const ( + Instruction_InitializeBuffer uint32 = iota + Instruction_Write + Instruction_DeployWithMaxDataLen + Instruction_Upgrade + Instruction_SetAuthority + Instruction_Close + Instruction_ExtendProgram + Instruction_SetAuthorityChecked +) + +func InstructionIDToName(id uint32) string { + switch id { + case Instruction_InitializeBuffer: + return "InitializeBuffer" + case Instruction_Write: + return "Write" + case Instruction_DeployWithMaxDataLen: + return "DeployWithMaxDataLen" + case Instruction_Upgrade: + return "Upgrade" + case Instruction_SetAuthority: + return "SetAuthority" + case Instruction_Close: + return "Close" + case Instruction_ExtendProgram: + return "ExtendProgram" + case Instruction_SetAuthorityChecked: + return "SetAuthorityChecked" + default: + return "" + } +} + +type Instruction struct { + ag_binary.BaseVariant +} + +func (inst *Instruction) EncodeToTree(parent ag_treeout.Branches) { + if enToTree, ok := inst.Impl.(ag_text.EncodableToTree); ok { + enToTree.EncodeToTree(parent) + } else { + parent.Child(ag_spew.Sdump(inst)) + } +} + +var InstructionImplDef = ag_binary.NewVariantDefinition( + ag_binary.Uint32TypeIDEncoding, + []ag_binary.VariantType{ + {"InitializeBuffer", (*InitializeBuffer)(nil)}, + {"Write", (*Write)(nil)}, + {"DeployWithMaxDataLen", (*DeployWithMaxDataLen)(nil)}, + {"Upgrade", (*Upgrade)(nil)}, + {"SetAuthority", (*SetAuthority)(nil)}, + {"Close", (*Close)(nil)}, + {"ExtendProgram", (*ExtendProgram)(nil)}, + {"SetAuthorityChecked", (*SetAuthorityChecked)(nil)}, + }, +) + +func (inst *Instruction) ProgramID() ag_solanago.PublicKey { + return ProgramID +} + +func (inst *Instruction) Accounts() (out []*ag_solanago.AccountMeta) { + return inst.Impl.(ag_solanago.AccountsGettable).GetAccounts() +} + +func (inst *Instruction) Data() ([]byte, error) { + buf := new(bytes.Buffer) + if err := ag_binary.NewBinEncoder(buf).Encode(inst); err != nil { + return nil, fmt.Errorf("unable to encode instruction: %w", err) + } + return buf.Bytes(), nil +} + +func (inst *Instruction) TextEncode(encoder *ag_text.Encoder, option *ag_text.Option) error { + return encoder.Encode(inst.Impl, option) +} + +func (inst *Instruction) UnmarshalWithDecoder(decoder *ag_binary.Decoder) error { + return inst.BaseVariant.UnmarshalBinaryVariant(decoder, InstructionImplDef) +} + +func (inst Instruction) MarshalWithEncoder(encoder *ag_binary.Encoder) error { + if err := encoder.WriteUint32(inst.TypeID.Uint32(), binary.LittleEndian); err != nil { + return fmt.Errorf("unable to write variant type: %w", err) + } + return encoder.Encode(inst.Impl) +} + +func registryDecodeInstruction(accounts []*ag_solanago.AccountMeta, data []byte) (any, error) { + return DecodeInstruction(accounts, data) +} + +func DecodeInstruction(accounts []*ag_solanago.AccountMeta, data []byte) (*Instruction, error) { + inst := new(Instruction) + if err := ag_binary.NewBinDecoder(data).Decode(inst); err != nil { + return nil, fmt.Errorf("unable to decode instruction: %w", err) + } + if v, ok := inst.Impl.(ag_solanago.AccountsSettable); ok { + if err := v.SetAccounts(accounts); err != nil { + return nil, fmt.Errorf("unable to set accounts for instruction: %w", err) + } + } + return inst, nil +} + +// readOptionalTrailingBool implements the OptionalTrailingBool +// wincode schema used by SIMD-0430/0432: consume one byte from the decoder +// if any remain, otherwise fall back to the provided default. +func readOptionalTrailingBool(decoder *ag_binary.Decoder, def bool) (bool, error) { + if !decoder.HasRemaining() { + return def, nil + } + b, err := decoder.ReadByte() + if err != nil { + return false, err + } + switch b { + case 0: + return false, nil + case 1: + return true, nil + default: + return false, fmt.Errorf("%w: byte %d", ErrInvalidBoolEncoding, b) + } +} diff --git a/programs/loader-v3/instructions_test.go b/programs/loader-v3/instructions_test.go new file mode 100644 index 000000000..a29b2a519 --- /dev/null +++ b/programs/loader-v3/instructions_test.go @@ -0,0 +1,253 @@ +// Copyright 2021 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 loaderv3 + +import ( + "errors" + "testing" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" +) + +// Placeholder accounts for builder-driven tests. The upstream account-meta +// predicates are validated separately per instruction; these golden tests +// focus on wire bytes. +var ( + testBuffer = ag_solanago.MustPublicKeyFromBase58("7QcXLBB23bJ4q5QUXpxLkQBr37g8mNEPSSPyVvU22qUS") + testAuthority = ag_solanago.MustPublicKeyFromBase58("5fjG93skfVqRnF5M3n8h3fLvj1Fq1VmHnT1tqYfQPVpF") + testPayer = ag_solanago.MustPublicKeyFromBase58("3UVYmECPPMZSCqWKfENfuoTv51fTDTWicX9xmBD2euKe") + testProgram = ag_solanago.MustPublicKeyFromBase58("4Nd1mZjsqhMzGPhFHZ4mJ4nZxkFwVoQCpT8xcVFt5Kfr") + testSpill = ag_solanago.MustPublicKeyFromBase58("6WQZ5tL94vnB2G89fjCH6WVDSSz8Q6PF8U9X3vBt8aJH") + testNewAuth = ag_solanago.MustPublicKeyFromBase58("8U1JpQ4Z6GMg5Xdz5r78KjWqytBxEQNkTP2Xb6RRjXgH") +) + +// buildData is a helper that invokes an instruction's full Data() path, +// including the u32 LE discriminant prefix. +func buildData(t *testing.T, inst *Instruction) []byte { + t.Helper() + data, err := inst.Data() + require.NoError(t, err) + return data +} + +// TestWireCompat_Bincode reproduces the upstream `wire_compat_bincode_vs_wincode` +// test cases (13 variants) by asserting the exact byte layout emitted by each +// instruction's Data() matches the default bincode encoding: +// +// u32 LE discriminant, u64 LE Vec length, 1-byte bool, 32-byte Pubkey. +func TestWireCompat_Bincode(t *testing.T) { + tests := []struct { + name string + inst *Instruction + want []byte + }{ + { + name: "InitializeBuffer", + inst: NewInitializeBufferInstruction(testBuffer, testAuthority).Build(), + want: []byte{0x00, 0x00, 0x00, 0x00}, + }, + { + name: "Write(offset=42, bytes=[1..5])", + inst: NewWriteInstruction(testBuffer, testAuthority, 42, []byte{1, 2, 3, 4, 5}).Build(), + want: []byte{ + 0x01, 0x00, 0x00, 0x00, // disc + 0x2a, 0x00, 0x00, 0x00, // offset = 42 + 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // len = 5 + 0x01, 0x02, 0x03, 0x04, 0x05, + }, + }, + { + name: "Write(offset=0, bytes=[])", + inst: NewWriteInstruction(testBuffer, testAuthority, 0, []byte{}).Build(), + want: []byte{ + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }, + }, + { + name: "DeployWithMaxDataLen(1_000_000, close_buffer=true)", + inst: NewDeployWithMaxDataLenInstruction( + testPayer, MustGetProgramDataAddress(testProgram), testProgram, + testBuffer, testAuthority, 1_000_000, true, + ).Build(), + want: []byte{ + 0x02, 0x00, 0x00, 0x00, // disc + 0x40, 0x42, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, // 1_000_000 u64 LE + 0x01, // close_buffer + }, + }, + { + name: "DeployWithMaxDataLen(0, close_buffer=false)", + inst: NewDeployWithMaxDataLenInstruction( + testPayer, MustGetProgramDataAddress(testProgram), testProgram, + testBuffer, testAuthority, 0, false, + ).Build(), + want: []byte{ + 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, + }, + }, + { + name: "Upgrade(close_buffer=true)", + inst: NewUpgradeInstruction(testProgram, testBuffer, testAuthority, testSpill, true).Build(), + want: []byte{0x03, 0x00, 0x00, 0x00, 0x01}, + }, + { + name: "Upgrade(close_buffer=false)", + inst: NewUpgradeInstruction(testProgram, testBuffer, testAuthority, testSpill, false).Build(), + want: []byte{0x03, 0x00, 0x00, 0x00, 0x00}, + }, + { + name: "SetAuthority", + inst: NewSetBufferAuthorityInstruction(testBuffer, testAuthority, testNewAuth).Build(), + want: []byte{0x04, 0x00, 0x00, 0x00}, + }, + { + name: "Close(tombstone=false)", + inst: NewCloseInstruction(testBuffer, testPayer, testAuthority, false).Build(), + want: []byte{0x05, 0x00, 0x00, 0x00, 0x00}, + }, + { + name: "Close(tombstone=true)", + inst: NewCloseInstruction(testBuffer, testPayer, testAuthority, true).Build(), + want: []byte{0x05, 0x00, 0x00, 0x00, 0x01}, + }, + { + name: "ExtendProgram(10_240)", + inst: NewExtendProgramInstruction(testProgram, nil, 10_240).Build(), + want: []byte{ + 0x06, 0x00, 0x00, 0x00, + 0x00, 0x28, 0x00, 0x00, // 10240 u32 LE + }, + }, + { + name: "ExtendProgram(0)", + inst: NewExtendProgramInstruction(testProgram, nil, 0).Build(), + want: []byte{ + 0x06, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + }, + }, + { + name: "SetAuthorityChecked", + inst: NewSetBufferAuthorityCheckedInstruction(testBuffer, testAuthority, testNewAuth).Build(), + want: []byte{0x07, 0x00, 0x00, 0x00}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := buildData(t, tc.inst) + require.Equal(t, tc.want, got) + + // Round-trip: the produced bytes must decode back to the same variant. + decoded, err := DecodeInstruction(tc.inst.Accounts(), got) + require.NoError(t, err) + require.NotNil(t, decoded.Impl) + }) + } +} + +// TestLegacyDeployDecodesCloseBufferAsTrue verifies the SIMD-0430 decode +// tolerance: a payload lacking the trailing close_buffer byte must decode as +// `close_buffer = true`. +func TestLegacyDeployDecodesCloseBufferAsTrue(t *testing.T) { + legacy := []byte{ + 0x02, 0x00, 0x00, 0x00, // disc = 2 + 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // max_data_len = 42 + } + decoded := new(Instruction) + require.NoError(t, ag_binary.NewBinDecoder(legacy).Decode(decoded)) + + impl, ok := decoded.Impl.(*DeployWithMaxDataLen) + require.True(t, ok, "expected *DeployWithMaxDataLen") + require.Equal(t, uint64(42), *impl.MaxDataLen) + require.True(t, impl.CloseBuffer, "close_buffer should default to true on exhausted read") +} + +func TestLegacyUpgradeDecodesCloseBufferAsTrue(t *testing.T) { + legacy := []byte{0x03, 0x00, 0x00, 0x00} // bare discriminant + decoded := new(Instruction) + require.NoError(t, ag_binary.NewBinDecoder(legacy).Decode(decoded)) + + impl, ok := decoded.Impl.(*Upgrade) + require.True(t, ok) + require.True(t, impl.CloseBuffer) +} + +// TestLegacyCloseDecodesTombstoneAsFalse verifies the SIMD-0432 default. +func TestLegacyCloseDecodesTombstoneAsFalse(t *testing.T) { + legacy := []byte{0x05, 0x00, 0x00, 0x00} + decoded := new(Instruction) + require.NoError(t, ag_binary.NewBinDecoder(legacy).Decode(decoded)) + + impl, ok := decoded.Impl.(*Close) + require.True(t, ok) + require.False(t, impl.Tombstone) +} + +// TestInvalidOptionalTrailingBoolByteErrors mirrors upstream's error-path +// test: any byte other than 0 or 1 in an OptionalTrailingBool slot must +// surface as a decode error. The top-level Instruction path is exercised +// here; errors.Is against ErrInvalidBoolEncoding is covered separately by +// TestInvalidOptionalTrailingBool_Sentinel, which uses the direct-variant +// decode path where the error chain is preserved (ag_binary's variant +// dispatcher wraps with %s, which strips %w context). +func TestInvalidOptionalTrailingBoolByteErrors(t *testing.T) { + cases := [][]byte{ + {0x02, 0x00, 0x00, 0x00, 0x2a, 0, 0, 0, 0, 0, 0, 0, 0x02}, // DeployWithMaxDataLen + {0x03, 0x00, 0x00, 0x00, 0x02}, // Upgrade + {0x05, 0x00, 0x00, 0x00, 0x02}, // Close + } + for i, data := range cases { + decoded := new(Instruction) + err := ag_binary.NewBinDecoder(data).Decode(decoded) + require.Error(t, err, "case %d should fail", i) + } +} + +// TestInvalidOptionalTrailingBool_Sentinel verifies that direct-variant +// decoders surface ErrInvalidBoolEncoding so callers can match on it via +// errors.Is. This is the path taken when a caller already knows the +// concrete variant (e.g. after inspecting the discriminant themselves). +func TestInvalidOptionalTrailingBool_Sentinel(t *testing.T) { + // Upgrade's trailing bool byte = 0x02 — invalid. + dec := ag_binary.NewBinDecoder([]byte{0x02}) + got := new(Upgrade) + err := got.UnmarshalWithDecoder(dec) + require.Error(t, err) + require.True(t, errors.Is(err, ErrInvalidBoolEncoding), + "expected ErrInvalidBoolEncoding, got %v", err) +} + +func TestInstructionIDToName(t *testing.T) { + require.Equal(t, "InitializeBuffer", InstructionIDToName(Instruction_InitializeBuffer)) + require.Equal(t, "Write", InstructionIDToName(Instruction_Write)) + require.Equal(t, "DeployWithMaxDataLen", InstructionIDToName(Instruction_DeployWithMaxDataLen)) + require.Equal(t, "Upgrade", InstructionIDToName(Instruction_Upgrade)) + require.Equal(t, "SetAuthority", InstructionIDToName(Instruction_SetAuthority)) + require.Equal(t, "Close", InstructionIDToName(Instruction_Close)) + require.Equal(t, "ExtendProgram", InstructionIDToName(Instruction_ExtendProgram)) + require.Equal(t, "SetAuthorityChecked", InstructionIDToName(Instruction_SetAuthorityChecked)) + require.Equal(t, "", InstructionIDToName(99)) +} + +func TestMinimumExtendProgramBytes(t *testing.T) { + require.Equal(t, uint32(10_240), MINIMUM_EXTEND_PROGRAM_BYTES) +} diff --git a/programs/loader-v3/pda.go b/programs/loader-v3/pda.go new file mode 100644 index 000000000..0fbabefb0 --- /dev/null +++ b/programs/loader-v3/pda.go @@ -0,0 +1,40 @@ +// Copyright 2021 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 loaderv3 + +import ( + ag_solanago "github.com/gagliardetto/solana-go" +) + +// GetProgramDataAddress derives the PDA that stores a program's data for a +// given program account, matching the upstream helper: +// +// Pubkey::find_program_address(&[program.as_ref()], &bpf_loader_upgradeable::id()) +func GetProgramDataAddress(programAddress ag_solanago.PublicKey) (ag_solanago.PublicKey, uint8, error) { + return ag_solanago.FindProgramAddress( + [][]byte{programAddress[:]}, + ProgramID, + ) +} + +// MustGetProgramDataAddress panics on error. Convenience for callers that +// trust the inputs (e.g. tests, hard-coded program ids). +func MustGetProgramDataAddress(programAddress ag_solanago.PublicKey) ag_solanago.PublicKey { + addr, _, err := GetProgramDataAddress(programAddress) + if err != nil { + panic(err) + } + return addr +} diff --git a/programs/loader-v3/state.go b/programs/loader-v3/state.go new file mode 100644 index 000000000..5b73e70a0 --- /dev/null +++ b/programs/loader-v3/state.go @@ -0,0 +1,187 @@ +// Copyright 2021 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 loaderv3 + +import ( + "encoding/binary" + "fmt" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" +) + +// State discriminants for UpgradeableLoaderState (u32 LE). +const ( + State_Uninitialized uint32 = iota + State_Buffer + State_Program + State_ProgramData +) + +// Size constants for the serialized UpgradeableLoaderState header, matching +// upstream's `UpgradeableLoaderState::size_of_*` helpers. +const ( + SizeOfUninitialized = 4 // u32 discriminant + SizeOfBufferMetadata = 37 // disc(4) + Option tag(1) + 32 + SizeOfProgram = 36 // disc(4) + Pubkey(32) + SizeOfProgramDataMetadata = 45 // disc(4) + slot(8) + Option tag(1) + 32 +) + +// SizeOfBuffer returns the account size required to hold a Buffer of the +// given program length. +func SizeOfBuffer(programLen int) int { return SizeOfBufferMetadata + programLen } + +// SizeOfProgramData returns the account size required to hold ProgramData of +// the given program length. +func SizeOfProgramData(programLen int) int { return SizeOfProgramDataMetadata + programLen } + +// UpgradeableLoaderState is the tagged-union account payload for buffer, +// program, and programdata accounts owned by BPFLoaderUpgradeab1e. +// Exactly one of the Buffer/Program/ProgramData pointer fields is populated +// after a successful decode. Uninitialized is represented by all fields nil. +type UpgradeableLoaderState struct { + Buffer *StateBuffer + Program *StateProgram + ProgramData *StateProgramData +} + +type StateBuffer struct { + // Authority address for the buffer; nil means None. + AuthorityAddress *ag_solanago.PublicKey +} + +type StateProgram struct { + ProgramDataAddress ag_solanago.PublicKey +} + +type StateProgramData struct { + Slot uint64 + // nil means None (final/immutable ProgramData). + UpgradeAuthorityAddress *ag_solanago.PublicKey +} + +// IsUninitialized reports whether the state is the Uninitialized variant. +func (s *UpgradeableLoaderState) IsUninitialized() bool { + return s.Buffer == nil && s.Program == nil && s.ProgramData == nil +} + +// MarshalWithEncoder serializes the state with upstream's bincode layout: +// u32 LE discriminant followed by the variant payload. +func (s UpgradeableLoaderState) MarshalWithEncoder(encoder *ag_binary.Encoder) error { + set := 0 + if s.Buffer != nil { + set++ + } + if s.Program != nil { + set++ + } + if s.ProgramData != nil { + set++ + } + if set > 1 { + return fmt.Errorf("UpgradeableLoaderState has multiple variants set") + } + + switch { + case s.Buffer != nil: + if err := encoder.WriteUint32(State_Buffer, binary.LittleEndian); err != nil { + return err + } + return writeOptionPubkey(encoder, s.Buffer.AuthorityAddress) + case s.Program != nil: + if err := encoder.WriteUint32(State_Program, binary.LittleEndian); err != nil { + return err + } + return encoder.Encode(s.Program.ProgramDataAddress) + case s.ProgramData != nil: + if err := encoder.WriteUint32(State_ProgramData, binary.LittleEndian); err != nil { + return err + } + if err := encoder.WriteUint64(s.ProgramData.Slot, binary.LittleEndian); err != nil { + return err + } + return writeOptionPubkey(encoder, s.ProgramData.UpgradeAuthorityAddress) + default: + return encoder.WriteUint32(State_Uninitialized, binary.LittleEndian) + } +} + +func (s *UpgradeableLoaderState) UnmarshalWithDecoder(decoder *ag_binary.Decoder) error { + disc, err := decoder.ReadUint32(binary.LittleEndian) + if err != nil { + return err + } + *s = UpgradeableLoaderState{} + switch disc { + case State_Uninitialized: + return nil + case State_Buffer: + auth, err := readOptionPubkey(decoder) + if err != nil { + return err + } + s.Buffer = &StateBuffer{AuthorityAddress: auth} + return nil + case State_Program: + var pda ag_solanago.PublicKey + if err := decoder.Decode(&pda); err != nil { + return err + } + s.Program = &StateProgram{ProgramDataAddress: pda} + return nil + case State_ProgramData: + slot, err := decoder.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + auth, err := readOptionPubkey(decoder) + if err != nil { + return err + } + s.ProgramData = &StateProgramData{Slot: slot, UpgradeAuthorityAddress: auth} + return nil + default: + return fmt.Errorf("unknown UpgradeableLoaderState discriminant: %d", disc) + } +} + +func writeOptionPubkey(encoder *ag_binary.Encoder, pk *ag_solanago.PublicKey) error { + if pk == nil { + return encoder.WriteUint8(0) + } + if err := encoder.WriteUint8(1); err != nil { + return err + } + return encoder.Encode(*pk) +} + +func readOptionPubkey(decoder *ag_binary.Decoder) (*ag_solanago.PublicKey, error) { + tag, err := decoder.ReadUint8() + if err != nil { + return nil, err + } + switch tag { + case 0: + return nil, nil + case 1: + var pk ag_solanago.PublicKey + if err := decoder.Decode(&pk); err != nil { + return nil, err + } + return &pk, nil + default: + return nil, fmt.Errorf("invalid Option tag: %d", tag) + } +} diff --git a/programs/loader-v3/state_test.go b/programs/loader-v3/state_test.go new file mode 100644 index 000000000..afacb468a --- /dev/null +++ b/programs/loader-v3/state_test.go @@ -0,0 +1,134 @@ +// Copyright 2021 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 loaderv3 + +import ( + "bytes" + "testing" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" +) + +// Mirrors upstream's `test_state_size_of_*` and `wire_compat_bincode_vs_wincode` +// (state) tests: asserts both the declared size constants and the actual +// serialized byte length of each variant. +func TestStateSizes(t *testing.T) { + require.Equal(t, 4, SizeOfUninitialized) + require.Equal(t, 37, SizeOfBufferMetadata) + require.Equal(t, 36, SizeOfProgram) + require.Equal(t, 45, SizeOfProgramDataMetadata) + + require.Equal(t, 37+128, SizeOfBuffer(128)) + require.Equal(t, 45+128, SizeOfProgramData(128)) +} + +func TestState_Uninitialized(t *testing.T) { + s := UpgradeableLoaderState{} + buf := new(bytes.Buffer) + require.NoError(t, ag_binary.NewBinEncoder(buf).Encode(s)) + require.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, buf.Bytes()) + + got := new(UpgradeableLoaderState) + require.NoError(t, ag_binary.NewBinDecoder(buf.Bytes()).Decode(got)) + require.True(t, got.IsUninitialized()) +} + +func TestState_Buffer_Some(t *testing.T) { + var defaultPubkey ag_solanago.PublicKey // zero value + s := UpgradeableLoaderState{ + Buffer: &StateBuffer{AuthorityAddress: &defaultPubkey}, + } + buf := new(bytes.Buffer) + require.NoError(t, ag_binary.NewBinEncoder(buf).Encode(s)) + require.Len(t, buf.Bytes(), SizeOfBufferMetadata) + + // Disc(4) + Option tag(1) + Pubkey(32) = 37 + require.Equal(t, byte(0x01), buf.Bytes()[0]) + require.Equal(t, byte(0x01), buf.Bytes()[4]) // Option = Some + + got := new(UpgradeableLoaderState) + require.NoError(t, ag_binary.NewBinDecoder(buf.Bytes()).Decode(got)) + require.NotNil(t, got.Buffer) + require.NotNil(t, got.Buffer.AuthorityAddress) + require.Equal(t, defaultPubkey, *got.Buffer.AuthorityAddress) +} + +func TestState_Buffer_None(t *testing.T) { + s := UpgradeableLoaderState{Buffer: &StateBuffer{AuthorityAddress: nil}} + buf := new(bytes.Buffer) + require.NoError(t, ag_binary.NewBinEncoder(buf).Encode(s)) + // Disc(4) + Option tag(1) = 5 + require.Equal(t, []byte{0x01, 0x00, 0x00, 0x00, 0x00}, buf.Bytes()) + + got := new(UpgradeableLoaderState) + require.NoError(t, ag_binary.NewBinDecoder(buf.Bytes()).Decode(got)) + require.NotNil(t, got.Buffer) + require.Nil(t, got.Buffer.AuthorityAddress) +} + +func TestState_Program(t *testing.T) { + var defaultPubkey ag_solanago.PublicKey + s := UpgradeableLoaderState{Program: &StateProgram{ProgramDataAddress: defaultPubkey}} + buf := new(bytes.Buffer) + require.NoError(t, ag_binary.NewBinEncoder(buf).Encode(s)) + require.Len(t, buf.Bytes(), SizeOfProgram) + require.Equal(t, byte(0x02), buf.Bytes()[0]) + + got := new(UpgradeableLoaderState) + require.NoError(t, ag_binary.NewBinDecoder(buf.Bytes()).Decode(got)) + require.NotNil(t, got.Program) + require.Equal(t, defaultPubkey, got.Program.ProgramDataAddress) +} + +func TestState_ProgramData_Some(t *testing.T) { + var defaultPubkey ag_solanago.PublicKey + s := UpgradeableLoaderState{ + ProgramData: &StateProgramData{ + Slot: 123_456_789, + UpgradeAuthorityAddress: &defaultPubkey, + }, + } + buf := new(bytes.Buffer) + require.NoError(t, ag_binary.NewBinEncoder(buf).Encode(s)) + require.Len(t, buf.Bytes(), SizeOfProgramDataMetadata) + require.Equal(t, byte(0x03), buf.Bytes()[0]) + + got := new(UpgradeableLoaderState) + require.NoError(t, ag_binary.NewBinDecoder(buf.Bytes()).Decode(got)) + require.NotNil(t, got.ProgramData) + require.Equal(t, uint64(123_456_789), got.ProgramData.Slot) + require.NotNil(t, got.ProgramData.UpgradeAuthorityAddress) +} + +func TestState_ProgramData_None(t *testing.T) { + s := UpgradeableLoaderState{ + ProgramData: &StateProgramData{Slot: 0, UpgradeAuthorityAddress: nil}, + } + buf := new(bytes.Buffer) + require.NoError(t, ag_binary.NewBinEncoder(buf).Encode(s)) + expected := []byte{ + 0x03, 0x00, 0x00, 0x00, // disc + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // slot + 0x00, // Option = None + } + require.Equal(t, expected, buf.Bytes()) + + got := new(UpgradeableLoaderState) + require.NoError(t, ag_binary.NewBinDecoder(buf.Bytes()).Decode(got)) + require.NotNil(t, got.ProgramData) + require.Nil(t, got.ProgramData.UpgradeAuthorityAddress) +} diff --git a/programs/loader-v4/Copy.go b/programs/loader-v4/Copy.go new file mode 100644 index 000000000..8e7b28394 --- /dev/null +++ b/programs/loader-v4/Copy.go @@ -0,0 +1,159 @@ +// Copyright 2021 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 loaderv4 + +import ( + "encoding/binary" + "errors" + "fmt" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// Copy splices bytes from a source program account into the destination +// program account without having to round-trip through `Write`. +// +// Account references: +// [0] = [WRITE] Destination program +// [1] = [SIGNER] Authority +// [2] = [] Source program +type Copy struct { + DestinationOffset *uint32 + SourceOffset *uint32 + Length *uint32 + + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewCopyInstructionBuilder() *Copy { + return &Copy{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 3), + } +} + +func (inst *Copy) SetDestinationOffset(v uint32) *Copy { inst.DestinationOffset = &v; return inst } +func (inst *Copy) SetSourceOffset(v uint32) *Copy { inst.SourceOffset = &v; return inst } +func (inst *Copy) SetLength(v uint32) *Copy { inst.Length = &v; return inst } + +func (inst *Copy) SetProgramAccount(program ag_solanago.PublicKey) *Copy { + inst.AccountMetaSlice[0] = ag_solanago.Meta(program).WRITE() + return inst +} + +func (inst *Copy) SetAuthority(authority ag_solanago.PublicKey) *Copy { + inst.AccountMetaSlice[1] = ag_solanago.Meta(authority).SIGNER() + return inst +} + +func (inst *Copy) SetSourceAccount(source ag_solanago.PublicKey) *Copy { + inst.AccountMetaSlice[2] = ag_solanago.Meta(source) + return inst +} + +func (inst Copy) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint32(Instruction_Copy, binary.LittleEndian), + }} +} + +func (inst Copy) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *Copy) Validate() error { + if inst.DestinationOffset == nil { + return errors.New("DestinationOffset parameter is not set") + } + if inst.SourceOffset == nil { + return errors.New("SourceOffset parameter is not set") + } + if inst.Length == nil { + return errors.New("Length parameter is not set") + } + for i, acc := range inst.AccountMetaSlice { + if acc == nil { + return fmt.Errorf("ins.AccountMetaSlice[%v] is not set", i) + } + } + return nil +} + +func (inst *Copy) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("Copy")). + ParentFunc(func(ib ag_treeout.Branches) { + ib.Child("Params").ParentFunc(func(p ag_treeout.Branches) { + p.Child(ag_format.Param("DestinationOffset", *inst.DestinationOffset)) + p.Child(ag_format.Param(" SourceOffset", *inst.SourceOffset)) + p.Child(ag_format.Param(" Length", *inst.Length)) + }) + ib.Child("Accounts").ParentFunc(func(a ag_treeout.Branches) { + a.Child(ag_format.Meta("Destination", inst.AccountMetaSlice[0])) + a.Child(ag_format.Meta(" Authority", inst.AccountMetaSlice[1])) + a.Child(ag_format.Meta(" Source", inst.AccountMetaSlice[2])) + }) + }) + }) +} + +func (inst Copy) MarshalWithEncoder(encoder *ag_binary.Encoder) error { + if err := encoder.WriteUint32(*inst.DestinationOffset, binary.LittleEndian); err != nil { + return err + } + if err := encoder.WriteUint32(*inst.SourceOffset, binary.LittleEndian); err != nil { + return err + } + return encoder.WriteUint32(*inst.Length, binary.LittleEndian) +} + +func (inst *Copy) UnmarshalWithDecoder(decoder *ag_binary.Decoder) error { + dst, err := decoder.ReadUint32(binary.LittleEndian) + if err != nil { + return err + } + inst.DestinationOffset = &dst + src, err := decoder.ReadUint32(binary.LittleEndian) + if err != nil { + return err + } + inst.SourceOffset = &src + l, err := decoder.ReadUint32(binary.LittleEndian) + if err != nil { + return err + } + inst.Length = &l + return nil +} + +func NewCopyInstruction( + program, authority, source ag_solanago.PublicKey, + destinationOffset, sourceOffset, length uint32, +) *Copy { + return NewCopyInstructionBuilder(). + SetProgramAccount(program). + SetAuthority(authority). + SetSourceAccount(source). + SetDestinationOffset(destinationOffset). + SetSourceOffset(sourceOffset). + SetLength(length) +} diff --git a/programs/loader-v4/Deploy.go b/programs/loader-v4/Deploy.go new file mode 100644 index 000000000..a6d4c075a --- /dev/null +++ b/programs/loader-v4/Deploy.go @@ -0,0 +1,112 @@ +// Copyright 2021 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 loaderv4 + +import ( + "encoding/binary" + "fmt" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// Deploy promotes a retracted program account into the `Deployed` state. +// The optional third account (set via NewDeployFromSourceInstruction) reads +// bytes from another program account instead of the target itself. +// +// Account references (two-account form): +// [0] = [WRITE] Program account +// [1] = [SIGNER] Authority +// +// Account references (deploy-from-source form): +// [0] = [WRITE] Program account +// [1] = [SIGNER] Authority +// [2] = [WRITE] Source program account +type Deploy struct { + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewDeployInstructionBuilder() *Deploy { + return &Deploy{} +} + +func (inst Deploy) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint32(Instruction_Deploy, binary.LittleEndian), + }} +} + +func (inst Deploy) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *Deploy) Validate() error { + if n := len(inst.AccountMetaSlice); n != 2 && n != 3 { + return fmt.Errorf("Deploy expects 2 or 3 accounts, got %d", n) + } + for i, acc := range inst.AccountMetaSlice { + if acc == nil { + return fmt.Errorf("ins.AccountMetaSlice[%v] is not set", i) + } + } + return nil +} + +func (inst *Deploy) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("Deploy")). + ParentFunc(func(ib ag_treeout.Branches) { + ib.Child("Accounts").ParentFunc(func(a ag_treeout.Branches) { + for i, acc := range inst.AccountMetaSlice { + a.Child(ag_format.Meta(fmt.Sprintf("Account[%d]", i), acc)) + } + }) + }) + }) +} + +func (inst Deploy) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } +func (inst *Deploy) UnmarshalWithDecoder(_ *ag_binary.Decoder) error { return nil } + +// NewDeployInstruction builds the two-account form: deploy the program +// using bytes already written to its own account. +func NewDeployInstruction(program, authority ag_solanago.PublicKey) *Deploy { + inst := NewDeployInstructionBuilder() + inst.AccountMetaSlice = ag_solanago.AccountMetaSlice{ + ag_solanago.Meta(program).WRITE(), + ag_solanago.Meta(authority).SIGNER(), + } + return inst +} + +// NewDeployFromSourceInstruction builds the three-account form: deploy the +// program using bytes from a separate source account, which is consumed in +// the process. +func NewDeployFromSourceInstruction(program, authority, source ag_solanago.PublicKey) *Deploy { + inst := NewDeployInstructionBuilder() + inst.AccountMetaSlice = ag_solanago.AccountMetaSlice{ + ag_solanago.Meta(program).WRITE(), + ag_solanago.Meta(authority).SIGNER(), + ag_solanago.Meta(source).WRITE(), + } + return inst +} diff --git a/programs/loader-v4/Finalize.go b/programs/loader-v4/Finalize.go new file mode 100644 index 000000000..e30b31a0b --- /dev/null +++ b/programs/loader-v4/Finalize.go @@ -0,0 +1,108 @@ +// Copyright 2021 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 loaderv4 + +import ( + "encoding/binary" + "fmt" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// Finalize promotes a Deployed program into the terminal Finalized state, +// making it immutable. The third account records a pointer to the +// next-version program (for upgrade discovery) without enforcing that +// pointer's semantics. +// +// Account references: +// [0] = [WRITE] Program account +// [1] = [SIGNER] Authority +// [2] = [] Next-version program account +type Finalize struct { + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewFinalizeInstructionBuilder() *Finalize { + return &Finalize{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 3), + } +} + +func (inst *Finalize) SetProgramAccount(program ag_solanago.PublicKey) *Finalize { + inst.AccountMetaSlice[0] = ag_solanago.Meta(program).WRITE() + return inst +} + +func (inst *Finalize) SetAuthority(authority ag_solanago.PublicKey) *Finalize { + inst.AccountMetaSlice[1] = ag_solanago.Meta(authority).SIGNER() + return inst +} + +func (inst *Finalize) SetNextVersionProgram(next ag_solanago.PublicKey) *Finalize { + inst.AccountMetaSlice[2] = ag_solanago.Meta(next) + return inst +} + +func (inst Finalize) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint32(Instruction_Finalize, binary.LittleEndian), + }} +} + +func (inst Finalize) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *Finalize) Validate() error { + for i, acc := range inst.AccountMetaSlice { + if acc == nil { + return fmt.Errorf("ins.AccountMetaSlice[%v] is not set", i) + } + } + return nil +} + +func (inst *Finalize) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("Finalize")). + ParentFunc(func(ib ag_treeout.Branches) { + ib.Child("Accounts").ParentFunc(func(a ag_treeout.Branches) { + a.Child(ag_format.Meta(" Program", inst.AccountMetaSlice[0])) + a.Child(ag_format.Meta(" Authority", inst.AccountMetaSlice[1])) + a.Child(ag_format.Meta("NextVersion", inst.AccountMetaSlice[2])) + }) + }) + }) +} + +func (inst Finalize) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } +func (inst *Finalize) UnmarshalWithDecoder(_ *ag_binary.Decoder) error { return nil } + +func NewFinalizeInstruction( + program, authority, nextVersionProgram ag_solanago.PublicKey, +) *Finalize { + return NewFinalizeInstructionBuilder(). + SetProgramAccount(program). + SetAuthority(authority). + SetNextVersionProgram(nextVersionProgram) +} diff --git a/programs/loader-v4/Retract.go b/programs/loader-v4/Retract.go new file mode 100644 index 000000000..5d89c75d3 --- /dev/null +++ b/programs/loader-v4/Retract.go @@ -0,0 +1,95 @@ +// Copyright 2021 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 loaderv4 + +import ( + "encoding/binary" + "fmt" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// Retract moves a Deployed program back to Retracted, re-enabling writes. +// +// Account references: +// [0] = [WRITE] Program account +// [1] = [SIGNER] Authority +type Retract struct { + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewRetractInstructionBuilder() *Retract { + return &Retract{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 2), + } +} + +func (inst *Retract) SetProgramAccount(program ag_solanago.PublicKey) *Retract { + inst.AccountMetaSlice[0] = ag_solanago.Meta(program).WRITE() + return inst +} + +func (inst *Retract) SetAuthority(authority ag_solanago.PublicKey) *Retract { + inst.AccountMetaSlice[1] = ag_solanago.Meta(authority).SIGNER() + return inst +} + +func (inst Retract) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint32(Instruction_Retract, binary.LittleEndian), + }} +} + +func (inst Retract) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *Retract) Validate() error { + for i, acc := range inst.AccountMetaSlice { + if acc == nil { + return fmt.Errorf("ins.AccountMetaSlice[%v] is not set", i) + } + } + return nil +} + +func (inst *Retract) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("Retract")). + ParentFunc(func(ib ag_treeout.Branches) { + ib.Child("Accounts").ParentFunc(func(a ag_treeout.Branches) { + a.Child(ag_format.Meta(" Program", inst.AccountMetaSlice[0])) + a.Child(ag_format.Meta("Authority", inst.AccountMetaSlice[1])) + }) + }) + }) +} + +func (inst Retract) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } +func (inst *Retract) UnmarshalWithDecoder(_ *ag_binary.Decoder) error { return nil } + +func NewRetractInstruction(program, authority ag_solanago.PublicKey) *Retract { + return NewRetractInstructionBuilder(). + SetProgramAccount(program). + SetAuthority(authority) +} diff --git a/programs/loader-v4/SetProgramLength.go b/programs/loader-v4/SetProgramLength.go new file mode 100644 index 000000000..f0aa24020 --- /dev/null +++ b/programs/loader-v4/SetProgramLength.go @@ -0,0 +1,152 @@ +// Copyright 2021 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 loaderv4 + +import ( + "encoding/binary" + "errors" + "fmt" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/programs/system" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// SetProgramLength resizes a program account. Setting the size to zero +// effectively closes the account and refunds its rent to the recipient. +// +// Account references: +// [0] = [WRITE] Program account +// [1] = [SIGNER] Authority +// [2] = [WRITE] Recipient (rent refund destination) +type SetProgramLength struct { + NewSize *uint32 + + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewSetProgramLengthInstructionBuilder() *SetProgramLength { + return &SetProgramLength{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 3), + } +} + +func (inst *SetProgramLength) SetNewSize(v uint32) *SetProgramLength { + inst.NewSize = &v + return inst +} + +func (inst *SetProgramLength) SetProgramAccount(program ag_solanago.PublicKey) *SetProgramLength { + inst.AccountMetaSlice[0] = ag_solanago.Meta(program).WRITE() + return inst +} + +func (inst *SetProgramLength) SetAuthority(authority ag_solanago.PublicKey) *SetProgramLength { + inst.AccountMetaSlice[1] = ag_solanago.Meta(authority).SIGNER() + return inst +} + +func (inst *SetProgramLength) SetRecipient(recipient ag_solanago.PublicKey) *SetProgramLength { + inst.AccountMetaSlice[2] = ag_solanago.Meta(recipient).WRITE() + return inst +} + +func (inst SetProgramLength) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint32(Instruction_SetProgramLength, binary.LittleEndian), + }} +} + +func (inst SetProgramLength) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *SetProgramLength) Validate() error { + if inst.NewSize == nil { + return errors.New("NewSize parameter is not set") + } + for i, acc := range inst.AccountMetaSlice { + if acc == nil { + return fmt.Errorf("ins.AccountMetaSlice[%v] is not set", i) + } + } + return nil +} + +func (inst *SetProgramLength) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("SetProgramLength")). + ParentFunc(func(ib ag_treeout.Branches) { + ib.Child("Params").ParentFunc(func(p ag_treeout.Branches) { + p.Child(ag_format.Param("NewSize", *inst.NewSize)) + }) + ib.Child("Accounts").ParentFunc(func(a ag_treeout.Branches) { + a.Child(ag_format.Meta(" Program", inst.AccountMetaSlice[0])) + a.Child(ag_format.Meta("Authority", inst.AccountMetaSlice[1])) + a.Child(ag_format.Meta("Recipient", inst.AccountMetaSlice[2])) + }) + }) + }) +} + +func (inst SetProgramLength) MarshalWithEncoder(encoder *ag_binary.Encoder) error { + return encoder.WriteUint32(*inst.NewSize, binary.LittleEndian) +} + +func (inst *SetProgramLength) UnmarshalWithDecoder(decoder *ag_binary.Decoder) error { + v, err := decoder.ReadUint32(binary.LittleEndian) + if err != nil { + return err + } + inst.NewSize = &v + return nil +} + +func NewSetProgramLengthInstruction( + program, authority, recipient ag_solanago.PublicKey, + newSize uint32, +) *SetProgramLength { + return NewSetProgramLengthInstructionBuilder(). + SetProgramAccount(program). + SetAuthority(authority). + SetRecipient(recipient). + SetNewSize(newSize) +} + +// NewCreateBufferInstructions mirrors the upstream `create_buffer` helper. +// It allocates a zero-size program account via the system program and then +// calls SetProgramLength to grow it to `newSize`. +func NewCreateBufferInstructions( + payer, buffer, authority, recipient ag_solanago.PublicKey, + lamports uint64, + newSize uint32, +) []ag_solanago.Instruction { + create := system.NewCreateAccountInstruction( + lamports, + 0, + ProgramID, + payer, + buffer, + ).Build() + spl := NewSetProgramLengthInstruction(buffer, authority, recipient, newSize).Build() + return []ag_solanago.Instruction{create, spl} +} diff --git a/programs/loader-v4/TransferAuthority.go b/programs/loader-v4/TransferAuthority.go new file mode 100644 index 000000000..e0f4b48d7 --- /dev/null +++ b/programs/loader-v4/TransferAuthority.go @@ -0,0 +1,104 @@ +// Copyright 2021 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 loaderv4 + +import ( + "encoding/binary" + "fmt" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// TransferAuthority hands a program over to a new authority; both the current +// and new authorities must sign (no unchecked variant exists for v4). +// +// Account references: +// [0] = [WRITE] Program account +// [1] = [SIGNER] Current authority +// [2] = [SIGNER] New authority +type TransferAuthority struct { + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewTransferAuthorityInstructionBuilder() *TransferAuthority { + return &TransferAuthority{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 3), + } +} + +func (inst *TransferAuthority) SetProgramAccount(program ag_solanago.PublicKey) *TransferAuthority { + inst.AccountMetaSlice[0] = ag_solanago.Meta(program).WRITE() + return inst +} + +func (inst *TransferAuthority) SetAuthority(authority ag_solanago.PublicKey) *TransferAuthority { + inst.AccountMetaSlice[1] = ag_solanago.Meta(authority).SIGNER() + return inst +} + +func (inst *TransferAuthority) SetNewAuthority(newAuthority ag_solanago.PublicKey) *TransferAuthority { + inst.AccountMetaSlice[2] = ag_solanago.Meta(newAuthority).SIGNER() + return inst +} + +func (inst TransferAuthority) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint32(Instruction_TransferAuthority, binary.LittleEndian), + }} +} + +func (inst TransferAuthority) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *TransferAuthority) Validate() error { + for i, acc := range inst.AccountMetaSlice { + if acc == nil { + return fmt.Errorf("ins.AccountMetaSlice[%v] is not set", i) + } + } + return nil +} + +func (inst *TransferAuthority) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("TransferAuthority")). + ParentFunc(func(ib ag_treeout.Branches) { + ib.Child("Accounts").ParentFunc(func(a ag_treeout.Branches) { + a.Child(ag_format.Meta(" Program", inst.AccountMetaSlice[0])) + a.Child(ag_format.Meta(" Authority", inst.AccountMetaSlice[1])) + a.Child(ag_format.Meta("NewAuthority", inst.AccountMetaSlice[2])) + }) + }) + }) +} + +func (inst TransferAuthority) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } +func (inst *TransferAuthority) UnmarshalWithDecoder(_ *ag_binary.Decoder) error { return nil } + +func NewTransferAuthorityInstruction(program, authority, newAuthority ag_solanago.PublicKey) *TransferAuthority { + return NewTransferAuthorityInstructionBuilder(). + SetProgramAccount(program). + SetAuthority(authority). + SetNewAuthority(newAuthority) +} diff --git a/programs/loader-v4/Write.go b/programs/loader-v4/Write.go new file mode 100644 index 000000000..dc3535a3a --- /dev/null +++ b/programs/loader-v4/Write.go @@ -0,0 +1,143 @@ +// Copyright 2021 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 loaderv4 + +import ( + "encoding/binary" + "errors" + "fmt" + "slices" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// Write copies a chunk of bytes into a program account. +// +// Account references: +// [0] = [WRITE] Program account +// [1] = [SIGNER] Authority +type Write struct { + Offset *uint32 + Bytes []byte + + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewWriteInstructionBuilder() *Write { + return &Write{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 2), + } +} + +func (inst *Write) SetOffset(v uint32) *Write { inst.Offset = &v; return inst } +func (inst *Write) SetBytes(b []byte) *Write { inst.Bytes = b; return inst } + +func (inst *Write) SetProgramAccount(program ag_solanago.PublicKey) *Write { + inst.AccountMetaSlice[0] = ag_solanago.Meta(program).WRITE() + return inst +} + +func (inst *Write) SetAuthority(authority ag_solanago.PublicKey) *Write { + inst.AccountMetaSlice[1] = ag_solanago.Meta(authority).SIGNER() + return inst +} + +func (inst Write) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint32(Instruction_Write, binary.LittleEndian), + }} +} + +func (inst Write) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *Write) Validate() error { + if inst.Offset == nil { + return errors.New("Offset parameter is not set") + } + for i, acc := range inst.AccountMetaSlice { + if acc == nil { + return fmt.Errorf("ins.AccountMetaSlice[%v] is not set", i) + } + } + return nil +} + +func (inst *Write) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("Write")). + ParentFunc(func(ib ag_treeout.Branches) { + ib.Child("Params").ParentFunc(func(p ag_treeout.Branches) { + p.Child(ag_format.Param("Offset", *inst.Offset)) + p.Child(ag_format.Param(" Bytes", fmt.Sprintf("%d bytes", len(inst.Bytes)))) + }) + ib.Child("Accounts").ParentFunc(func(a ag_treeout.Branches) { + a.Child(ag_format.Meta(" Program", inst.AccountMetaSlice[0])) + a.Child(ag_format.Meta("Authority", inst.AccountMetaSlice[1])) + }) + }) + }) +} + +func (inst Write) MarshalWithEncoder(encoder *ag_binary.Encoder) error { + if err := encoder.WriteUint32(*inst.Offset, binary.LittleEndian); err != nil { + return err + } + if err := encoder.WriteUint64(uint64(len(inst.Bytes)), binary.LittleEndian); err != nil { + return err + } + return encoder.WriteBytes(inst.Bytes, false) +} + +func (inst *Write) UnmarshalWithDecoder(decoder *ag_binary.Decoder) error { + offset, err := decoder.ReadUint32(binary.LittleEndian) + if err != nil { + return err + } + inst.Offset = &offset + length, err := decoder.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + bts, err := decoder.ReadNBytes(int(length)) + if err != nil { + return err + } + // Clone: ReadNBytes returns a subslice of the decoder's input, so + // retaining it here would alias whatever buffer the caller passed in. + inst.Bytes = slices.Clone(bts) + return nil +} + +func NewWriteInstruction( + program, authority ag_solanago.PublicKey, + offset uint32, + bytes []byte, +) *Write { + return NewWriteInstructionBuilder(). + SetProgramAccount(program). + SetAuthority(authority). + SetOffset(offset). + SetBytes(bytes) +} diff --git a/programs/loader-v4/accounts_test.go b/programs/loader-v4/accounts_test.go new file mode 100644 index 000000000..c07308e94 --- /dev/null +++ b/programs/loader-v4/accounts_test.go @@ -0,0 +1,119 @@ +// Copyright 2021 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 loaderv4 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// The following tests mirror upstream's per-builder unit tests +// (`test_*_instruction`): they verify the program ID, account count, and +// the (IsWritable, IsSigner) flags on every AccountMeta. + +func TestAccounts_Write(t *testing.T) { + inst := NewWriteInstruction(testProgram, testAuthority, 0, nil).Build() + require.Equal(t, ProgramID, inst.ProgramID()) + a := inst.Accounts() + require.Len(t, a, 2) + + require.Equal(t, testProgram, a[0].PublicKey) + require.True(t, a[0].IsWritable) + require.False(t, a[0].IsSigner) + + require.Equal(t, testAuthority, a[1].PublicKey) + require.False(t, a[1].IsWritable) + require.True(t, a[1].IsSigner) +} + +func TestAccounts_Copy(t *testing.T) { + inst := NewCopyInstruction(testProgram, testAuthority, testSource, 0, 0, 0).Build() + a := inst.Accounts() + require.Len(t, a, 3) + require.True(t, a[0].IsWritable && !a[0].IsSigner) + require.True(t, !a[1].IsWritable && a[1].IsSigner) + require.True(t, !a[2].IsWritable && !a[2].IsSigner) + require.Equal(t, testSource, a[2].PublicKey) +} + +func TestAccounts_SetProgramLength(t *testing.T) { + inst := NewSetProgramLengthInstruction(testProgram, testAuthority, testRecipient, 100).Build() + a := inst.Accounts() + require.Len(t, a, 3) + require.True(t, a[0].IsWritable && !a[0].IsSigner) + require.True(t, !a[1].IsWritable && a[1].IsSigner) + require.True(t, a[2].IsWritable && !a[2].IsSigner) + require.Equal(t, testRecipient, a[2].PublicKey) +} + +func TestAccounts_Deploy(t *testing.T) { + inst := NewDeployInstruction(testProgram, testAuthority).Build() + a := inst.Accounts() + require.Len(t, a, 2) + require.True(t, a[0].IsWritable && !a[0].IsSigner) + require.True(t, !a[1].IsWritable && a[1].IsSigner) +} + +func TestAccounts_DeployFromSource(t *testing.T) { + inst := NewDeployFromSourceInstruction(testProgram, testAuthority, testSource).Build() + a := inst.Accounts() + require.Len(t, a, 3) + require.True(t, a[0].IsWritable && !a[0].IsSigner) + require.True(t, !a[1].IsWritable && a[1].IsSigner) + require.True(t, a[2].IsWritable && !a[2].IsSigner) + require.Equal(t, testSource, a[2].PublicKey) +} + +func TestAccounts_Retract(t *testing.T) { + inst := NewRetractInstruction(testProgram, testAuthority).Build() + a := inst.Accounts() + require.Len(t, a, 2) + require.True(t, a[0].IsWritable && !a[0].IsSigner) + require.True(t, !a[1].IsWritable && a[1].IsSigner) +} + +func TestAccounts_TransferAuthority(t *testing.T) { + inst := NewTransferAuthorityInstruction(testProgram, testAuthority, testNewAuth).Build() + a := inst.Accounts() + require.Len(t, a, 3) + require.True(t, a[0].IsWritable && !a[0].IsSigner) + require.True(t, !a[1].IsWritable && a[1].IsSigner) + require.True(t, !a[2].IsWritable && a[2].IsSigner) + require.Equal(t, testNewAuth, a[2].PublicKey) +} + +func TestAccounts_Finalize(t *testing.T) { + inst := NewFinalizeInstruction(testProgram, testAuthority, testNextVer).Build() + a := inst.Accounts() + require.Len(t, a, 3) + require.True(t, a[0].IsWritable && !a[0].IsSigner) + require.True(t, !a[1].IsWritable && a[1].IsSigner) + require.True(t, !a[2].IsWritable && !a[2].IsSigner) + require.Equal(t, testNextVer, a[2].PublicKey) +} + +func TestCreateBufferInstructions(t *testing.T) { + insts := NewCreateBufferInstructions(testPayer, testBufferAcct, testAuthority, testRecipient, 1_000, 1024) + require.Len(t, insts, 2) + // The first instruction is a system create_account; the second is + // SetProgramLength, whose program ID must match this package. + require.Equal(t, ProgramID, insts[1].ProgramID()) + + spl := insts[1].Accounts() + require.Len(t, spl, 3) + require.Equal(t, testBufferAcct, spl[0].PublicKey) + require.True(t, spl[0].IsWritable) +} diff --git a/programs/loader-v4/init_test.go b/programs/loader-v4/init_test.go new file mode 100644 index 000000000..411f9460a --- /dev/null +++ b/programs/loader-v4/init_test.go @@ -0,0 +1,21 @@ +// Copyright 2020 dfuse Platform Inc. +// +// 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 loaderv4 + +import "github.com/streamingfast/logging" + +func init() { + logging.TestingOverride() +} diff --git a/programs/loader-v4/instructions.go b/programs/loader-v4/instructions.go new file mode 100644 index 000000000..391db8e8f --- /dev/null +++ b/programs/loader-v4/instructions.go @@ -0,0 +1,187 @@ +// Copyright 2021 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 loaderv4 implements client-side instruction builders for loader-v4 +// (program ID LoaderV411...), the forward-looking replacement for the +// upgradeable BPF loader. +// +// The wire format matches upstream's default bincode encoding (u32 LE +// discriminant, u64 LE Vec length). Although the Rust enum is marked +// `#[repr(u8)]`, bincode ignores that attribute and emits a u32 discriminant. +package loaderv4 + +import ( + "bytes" + "encoding/binary" + "fmt" + + ag_spew "github.com/davecgh/go-spew/spew" + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_text "github.com/gagliardetto/solana-go/text" + ag_treeout "github.com/gagliardetto/treeout" +) + +var ProgramID ag_solanago.PublicKey = ag_solanago.LoaderV4ProgramID + +func SetProgramID(pubkey ag_solanago.PublicKey) error { + ProgramID = pubkey + return ag_solanago.RegisterInstructionDecoder(ProgramID, registryDecodeInstruction) +} + +const ProgramName = "LoaderV4" + +func init() { + ag_solanago.MustRegisterInstructionDecoder(ProgramID, registryDecodeInstruction) +} + +// DEPLOYMENT_COOLDOWN_IN_SLOTS is the minimum number of slots that must pass +// between successive deploys of the same program (upstream constant). +const DEPLOYMENT_COOLDOWN_IN_SLOTS uint64 = 1 + +const ( + Instruction_Write uint32 = iota + Instruction_Copy + Instruction_SetProgramLength + Instruction_Deploy + Instruction_Retract + Instruction_TransferAuthority + Instruction_Finalize +) + +func InstructionIDToName(id uint32) string { + switch id { + case Instruction_Write: + return "Write" + case Instruction_Copy: + return "Copy" + case Instruction_SetProgramLength: + return "SetProgramLength" + case Instruction_Deploy: + return "Deploy" + case Instruction_Retract: + return "Retract" + case Instruction_TransferAuthority: + return "TransferAuthority" + case Instruction_Finalize: + return "Finalize" + default: + return "" + } +} + +type Instruction struct { + ag_binary.BaseVariant +} + +func (inst *Instruction) EncodeToTree(parent ag_treeout.Branches) { + if enToTree, ok := inst.Impl.(ag_text.EncodableToTree); ok { + enToTree.EncodeToTree(parent) + } else { + parent.Child(ag_spew.Sdump(inst)) + } +} + +var InstructionImplDef = ag_binary.NewVariantDefinition( + ag_binary.Uint32TypeIDEncoding, + []ag_binary.VariantType{ + {"Write", (*Write)(nil)}, + {"Copy", (*Copy)(nil)}, + {"SetProgramLength", (*SetProgramLength)(nil)}, + {"Deploy", (*Deploy)(nil)}, + {"Retract", (*Retract)(nil)}, + {"TransferAuthority", (*TransferAuthority)(nil)}, + {"Finalize", (*Finalize)(nil)}, + }, +) + +func (inst *Instruction) ProgramID() ag_solanago.PublicKey { + return ProgramID +} + +func (inst *Instruction) Accounts() (out []*ag_solanago.AccountMeta) { + return inst.Impl.(ag_solanago.AccountsGettable).GetAccounts() +} + +func (inst *Instruction) Data() ([]byte, error) { + buf := new(bytes.Buffer) + if err := ag_binary.NewBinEncoder(buf).Encode(inst); err != nil { + return nil, fmt.Errorf("unable to encode instruction: %w", err) + } + return buf.Bytes(), nil +} + +func (inst *Instruction) TextEncode(encoder *ag_text.Encoder, option *ag_text.Option) error { + return encoder.Encode(inst.Impl, option) +} + +func (inst *Instruction) UnmarshalWithDecoder(decoder *ag_binary.Decoder) error { + return inst.BaseVariant.UnmarshalBinaryVariant(decoder, InstructionImplDef) +} + +func (inst Instruction) MarshalWithEncoder(encoder *ag_binary.Encoder) error { + if err := encoder.WriteUint32(inst.TypeID.Uint32(), binary.LittleEndian); err != nil { + return fmt.Errorf("unable to write variant type: %w", err) + } + return encoder.Encode(inst.Impl) +} + +func registryDecodeInstruction(accounts []*ag_solanago.AccountMeta, data []byte) (any, error) { + return DecodeInstruction(accounts, data) +} + +func DecodeInstruction(accounts []*ag_solanago.AccountMeta, data []byte) (*Instruction, error) { + inst := new(Instruction) + if err := ag_binary.NewBinDecoder(data).Decode(inst); err != nil { + return nil, fmt.Errorf("unable to decode instruction: %w", err) + } + if v, ok := inst.Impl.(ag_solanago.AccountsSettable); ok { + if err := v.SetAccounts(accounts); err != nil { + return nil, fmt.Errorf("unable to set accounts for instruction: %w", err) + } + } + return inst, nil +} + +// IsWriteInstruction mirrors upstream's `is_write_instruction` helper: given +// a raw instruction-data slice, returns true iff the first byte matches the +// Write discriminant. The check works because all v4 discriminants fit in a +// single byte and bincode's u32 encoding puts the low byte first. +func IsWriteInstruction(data []byte) bool { + return len(data) > 0 && data[0] == byte(Instruction_Write) +} + +func IsCopyInstruction(data []byte) bool { + return len(data) > 0 && data[0] == byte(Instruction_Copy) +} + +func IsSetProgramLengthInstruction(data []byte) bool { + return len(data) > 0 && data[0] == byte(Instruction_SetProgramLength) +} + +func IsDeployInstruction(data []byte) bool { + return len(data) > 0 && data[0] == byte(Instruction_Deploy) +} + +func IsRetractInstruction(data []byte) bool { + return len(data) > 0 && data[0] == byte(Instruction_Retract) +} + +func IsTransferAuthorityInstruction(data []byte) bool { + return len(data) > 0 && data[0] == byte(Instruction_TransferAuthority) +} + +func IsFinalizeInstruction(data []byte) bool { + return len(data) > 0 && data[0] == byte(Instruction_Finalize) +} diff --git a/programs/loader-v4/instructions_test.go b/programs/loader-v4/instructions_test.go new file mode 100644 index 000000000..eb83f793d --- /dev/null +++ b/programs/loader-v4/instructions_test.go @@ -0,0 +1,154 @@ +// Copyright 2021 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 loaderv4 + +import ( + "testing" + + ag_solanago "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" +) + +var ( + testProgram = ag_solanago.MustPublicKeyFromBase58("4Nd1mZjsqhMzGPhFHZ4mJ4nZxkFwVoQCpT8xcVFt5Kfr") + testAuthority = ag_solanago.MustPublicKeyFromBase58("5fjG93skfVqRnF5M3n8h3fLvj1Fq1VmHnT1tqYfQPVpF") + testSource = ag_solanago.MustPublicKeyFromBase58("7QcXLBB23bJ4q5QUXpxLkQBr37g8mNEPSSPyVvU22qUS") + testRecipient = ag_solanago.MustPublicKeyFromBase58("6WQZ5tL94vnB2G89fjCH6WVDSSz8Q6PF8U9X3vBt8aJH") + testNewAuth = ag_solanago.MustPublicKeyFromBase58("8U1JpQ4Z6GMg5Xdz5r78KjWqytBxEQNkTP2Xb6RRjXgH") + testNextVer = ag_solanago.MustPublicKeyFromBase58("3UVYmECPPMZSCqWKfENfuoTv51fTDTWicX9xmBD2euKe") + testPayer = ag_solanago.MustPublicKeyFromBase58("9VBbBpcPLmJcAJfrj45dWqPuKePYzRnT24yUhGmBe7uB") + testBufferAcct = ag_solanago.MustPublicKeyFromBase58("2xnCAsCZ9kC2L9QH4Zh8nZPyJkNuHfKqG8f9oNTtK3Vr") +) + +// TestWireCompat_Bincode asserts each instruction's Data() matches the +// default bincode encoding: u32 LE discriminant followed by the fixed-width +// payload. Although LoaderV4Instruction is marked `#[repr(u8)]` in Rust, +// bincode ignores that and serializes the discriminant as u32 LE. +func TestWireCompat_Bincode(t *testing.T) { + tests := []struct { + name string + inst *Instruction + want []byte + }{ + { + name: "Write", + inst: NewWriteInstruction(testProgram, testAuthority, 42, []byte{1, 2, 3}).Build(), + want: []byte{ + 0x00, 0x00, 0x00, 0x00, // disc + 0x2a, 0x00, 0x00, 0x00, // offset = 42 + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // len = 3 + 0x01, 0x02, 0x03, + }, + }, + { + name: "Copy", + inst: NewCopyInstruction(testProgram, testAuthority, testSource, 10, 20, 30).Build(), + want: []byte{ + 0x01, 0x00, 0x00, 0x00, + 0x0a, 0x00, 0x00, 0x00, // dst_offset = 10 + 0x14, 0x00, 0x00, 0x00, // src_offset = 20 + 0x1e, 0x00, 0x00, 0x00, // length = 30 + }, + }, + { + name: "SetProgramLength", + inst: NewSetProgramLengthInstruction(testProgram, testAuthority, testRecipient, 100_000).Build(), + want: []byte{ + 0x02, 0x00, 0x00, 0x00, + 0xa0, 0x86, 0x01, 0x00, // 100_000 u32 LE + }, + }, + { + name: "Deploy", + inst: NewDeployInstruction(testProgram, testAuthority).Build(), + want: []byte{0x03, 0x00, 0x00, 0x00}, + }, + { + name: "Retract", + inst: NewRetractInstruction(testProgram, testAuthority).Build(), + want: []byte{0x04, 0x00, 0x00, 0x00}, + }, + { + name: "TransferAuthority", + inst: NewTransferAuthorityInstruction(testProgram, testAuthority, testNewAuth).Build(), + want: []byte{0x05, 0x00, 0x00, 0x00}, + }, + { + name: "Finalize", + inst: NewFinalizeInstruction(testProgram, testAuthority, testNextVer).Build(), + want: []byte{0x06, 0x00, 0x00, 0x00}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.inst.Data() + require.NoError(t, err) + require.Equal(t, tc.want, got) + + decoded, err := DecodeInstruction(tc.inst.Accounts(), got) + require.NoError(t, err) + require.NotNil(t, decoded.Impl) + }) + } +} + +func TestIsXxxInstructionPredicates(t *testing.T) { + // Each `is_*_instruction` helper checks only the first byte, relying on + // bincode's u32 LE discriminant putting the low byte at index 0. + write := buildData(t, NewWriteInstruction(testProgram, testAuthority, 0, nil).Build()) + require.True(t, IsWriteInstruction(write)) + require.False(t, IsCopyInstruction(write)) + + cp := buildData(t, NewCopyInstruction(testProgram, testAuthority, testSource, 0, 0, 0).Build()) + require.True(t, IsCopyInstruction(cp)) + + spl := buildData(t, NewSetProgramLengthInstruction(testProgram, testAuthority, testRecipient, 0).Build()) + require.True(t, IsSetProgramLengthInstruction(spl)) + + dep := buildData(t, NewDeployInstruction(testProgram, testAuthority).Build()) + require.True(t, IsDeployInstruction(dep)) + + ret := buildData(t, NewRetractInstruction(testProgram, testAuthority).Build()) + require.True(t, IsRetractInstruction(ret)) + + ta := buildData(t, NewTransferAuthorityInstruction(testProgram, testAuthority, testNewAuth).Build()) + require.True(t, IsTransferAuthorityInstruction(ta)) + + fin := buildData(t, NewFinalizeInstruction(testProgram, testAuthority, testNextVer).Build()) + require.True(t, IsFinalizeInstruction(fin)) + + // Empty data never matches anything. + require.False(t, IsWriteInstruction(nil)) + require.False(t, IsFinalizeInstruction(nil)) +} + +func buildData(t *testing.T, inst *Instruction) []byte { + t.Helper() + data, err := inst.Data() + require.NoError(t, err) + return data +} + +func TestInstructionIDToName(t *testing.T) { + require.Equal(t, "Write", InstructionIDToName(Instruction_Write)) + require.Equal(t, "Copy", InstructionIDToName(Instruction_Copy)) + require.Equal(t, "SetProgramLength", InstructionIDToName(Instruction_SetProgramLength)) + require.Equal(t, "Deploy", InstructionIDToName(Instruction_Deploy)) + require.Equal(t, "Retract", InstructionIDToName(Instruction_Retract)) + require.Equal(t, "TransferAuthority", InstructionIDToName(Instruction_TransferAuthority)) + require.Equal(t, "Finalize", InstructionIDToName(Instruction_Finalize)) + require.Equal(t, "", InstructionIDToName(99)) +} diff --git a/programs/loader-v4/state.go b/programs/loader-v4/state.go new file mode 100644 index 000000000..ed4e484e7 --- /dev/null +++ b/programs/loader-v4/state.go @@ -0,0 +1,83 @@ +// Copyright 2021 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 loaderv4 + +import ( + "encoding/binary" + "fmt" + + ag_solanago "github.com/gagliardetto/solana-go" +) + +// LoaderV4Status mirrors `#[repr(u64)]` upstream. Serialized as u64 LE. +type LoaderV4Status uint64 + +const ( + StatusRetracted LoaderV4Status = 0 + StatusDeployed LoaderV4Status = 1 + StatusFinalized LoaderV4Status = 2 +) + +func (s LoaderV4Status) String() string { + switch s { + case StatusRetracted: + return "Retracted" + case StatusDeployed: + return "Deployed" + case StatusFinalized: + return "Finalized" + default: + return fmt.Sprintf("Unknown(%d)", uint64(s)) + } +} + +// ProgramDataOffset is the fixed byte offset at which program bytes begin in +// a loader-v4 account. It equals the in-memory size of LoaderV4State. +const ProgramDataOffset = 48 + +// LoaderV4State is read directly (zero-copy) from the start of a loader-v4 +// account. Unlike loader-v3 state, this struct is NOT bincode-encoded — +// upstream uses `#[repr(C)]` and casts the account data pointer. The layout +// is u64 slot | [32]u8 authority | u64 status. +type LoaderV4State struct { + Slot uint64 + AuthorityAddressOrNextVersion ag_solanago.PublicKey + Status LoaderV4Status +} + +// UnpackLoaderV4State parses the 48-byte header at the start of a loader-v4 +// account's data. Extra bytes after the header are ignored (they are the +// program ELF payload). +func UnpackLoaderV4State(data []byte) (*LoaderV4State, error) { + if len(data) < ProgramDataOffset { + return nil, fmt.Errorf("loader-v4 state too short: %d < %d", len(data), ProgramDataOffset) + } + s := &LoaderV4State{ + Slot: binary.LittleEndian.Uint64(data[0:8]), + Status: LoaderV4Status(binary.LittleEndian.Uint64(data[40:48])), + } + copy(s.AuthorityAddressOrNextVersion[:], data[8:40]) + return s, nil +} + +// Pack emits the 48-byte header in upstream's `#[repr(C)]` layout. Callers +// that also need program bytes should append them to the returned slice. +func (s *LoaderV4State) Pack() []byte { + out := make([]byte, ProgramDataOffset) + binary.LittleEndian.PutUint64(out[0:8], s.Slot) + copy(out[8:40], s.AuthorityAddressOrNextVersion[:]) + binary.LittleEndian.PutUint64(out[40:48], uint64(s.Status)) + return out +} diff --git a/programs/loader-v4/state_test.go b/programs/loader-v4/state_test.go new file mode 100644 index 000000000..5d4cf13bf --- /dev/null +++ b/programs/loader-v4/state_test.go @@ -0,0 +1,76 @@ +// Copyright 2021 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 loaderv4 + +import ( + "testing" + + ag_solanago "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" +) + +// TestLayout mirrors upstream's `test_layout`: +// +// slot @ 0x00, authority @ 0x08, status @ 0x28, program_data_offset == 0x30. +func TestLayout(t *testing.T) { + require.Equal(t, 48, ProgramDataOffset) + + auth := ag_solanago.MustPublicKeyFromBase58("5fjG93skfVqRnF5M3n8h3fLvj1Fq1VmHnT1tqYfQPVpF") + state := &LoaderV4State{ + Slot: 0x11_22_33_44_55_66_77_88, + AuthorityAddressOrNextVersion: auth, + Status: StatusDeployed, + } + packed := state.Pack() + require.Len(t, packed, 48) + + // slot @ 0x00..0x08 + require.Equal(t, byte(0x88), packed[0]) + require.Equal(t, byte(0x11), packed[7]) + + // authority @ 0x08..0x28 + require.Equal(t, auth[:], packed[8:40]) + + // status @ 0x28..0x30 (u64 LE, value 1 = Deployed) + require.Equal(t, byte(0x01), packed[40]) + require.Equal(t, byte(0x00), packed[47]) + + // Round-trip. + got, err := UnpackLoaderV4State(packed) + require.NoError(t, err) + require.Equal(t, state.Slot, got.Slot) + require.Equal(t, state.AuthorityAddressOrNextVersion, got.AuthorityAddressOrNextVersion) + require.Equal(t, state.Status, got.Status) +} + +func TestUnpack_ShortData(t *testing.T) { + _, err := UnpackLoaderV4State(make([]byte, 47)) + require.Error(t, err) +} + +func TestUnpack_WithProgramBytes(t *testing.T) { + state := &LoaderV4State{Status: StatusFinalized} + payload := append(state.Pack(), []byte{0xAB, 0xCD, 0xEF}...) + got, err := UnpackLoaderV4State(payload) + require.NoError(t, err) + require.Equal(t, StatusFinalized, got.Status) +} + +func TestStatusString(t *testing.T) { + require.Equal(t, "Retracted", StatusRetracted.String()) + require.Equal(t, "Deployed", StatusDeployed.String()) + require.Equal(t, "Finalized", StatusFinalized.String()) + require.Equal(t, "Unknown(42)", LoaderV4Status(42).String()) +} From 7cf22dbcdccc56dd1156b4fbebe542a217a9566b Mon Sep 17 00:00:00 2001 From: Sonic Date: Thu, 23 Apr 2026 14:20:24 +0300 Subject: [PATCH 2/4] fix(message): use gojson --- message.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/message.go b/message.go index d7a382eee..b37a0ddbe 100644 --- a/message.go +++ b/message.go @@ -23,7 +23,7 @@ import ( bin "github.com/gagliardetto/binary" "github.com/gagliardetto/treeout" - jsoniter "github.com/json-iterator/go" + gojson "github.com/goccy/go-json" "github.com/gagliardetto/solana-go/text" ) @@ -242,7 +242,7 @@ func (mx *Message) UnmarshalJSON(data []byte) error { Header MessageHeader `json:"header"` RecentBlockhash Hash `json:"recentBlockhash"` Instructions []CompiledInstruction `json:"instructions"` - AddressTableLookups *jsoniter.RawMessage `json:"addressTableLookups"` + AddressTableLookups *gojson.RawMessage `json:"addressTableLookups"` }{} if err := json.Unmarshal(data, &aux); err != nil { return err From d228859b3b9d81f55b12ba11e79197ec294da056 Mon Sep 17 00:00:00 2001 From: Sonic Date: Thu, 30 Apr 2026 00:53:46 +0300 Subject: [PATCH 3/4] chore: lint fixes --- programs/loader-v2/Finalize.go | 7 ++++--- programs/loader-v2/Write.go | 7 ++++--- programs/loader-v2/instructions.go | 4 ++-- programs/loader-v3/Close.go | 9 +++++---- programs/loader-v3/DeployWithMaxDataLen.go | 17 +++++++++-------- programs/loader-v3/ExtendProgram.go | 9 +++++---- programs/loader-v3/InitializeBuffer.go | 9 +++++---- programs/loader-v3/SetAuthority.go | 20 +++++++++++--------- programs/loader-v3/SetAuthorityChecked.go | 9 +++++---- programs/loader-v3/Upgrade.go | 15 ++++++++------- programs/loader-v3/Write.go | 7 ++++--- programs/loader-v3/accounts_test.go | 3 +-- programs/loader-v3/instructions.go | 16 ++++++++-------- programs/loader-v3/state.go | 8 ++++---- programs/loader-v4/Copy.go | 9 +++++---- programs/loader-v4/Deploy.go | 14 ++++++++------ programs/loader-v4/Finalize.go | 9 +++++---- programs/loader-v4/Retract.go | 7 ++++--- programs/loader-v4/SetProgramLength.go | 7 ++++--- programs/loader-v4/TransferAuthority.go | 9 +++++---- programs/loader-v4/Write.go | 7 ++++--- programs/loader-v4/instructions.go | 14 +++++++------- 22 files changed, 117 insertions(+), 99 deletions(-) diff --git a/programs/loader-v2/Finalize.go b/programs/loader-v2/Finalize.go index 2cd50b987..18e210ae3 100644 --- a/programs/loader-v2/Finalize.go +++ b/programs/loader-v2/Finalize.go @@ -27,8 +27,9 @@ import ( // Finalize an account loaded with program data for execution. // // Account references: -// [0] = [WRITE, SIGNER] Account to prepare for execution -// [1] = [] Rent sysvar +// +// [0] = [WRITE, SIGNER] Account to prepare for execution +// [1] = [] Rent sysvar type Finalize struct { ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` } @@ -97,7 +98,7 @@ func (inst *Finalize) EncodeToTree(parent ag_treeout.Branches) { } // Finalize carries no payload: the discriminant alone is the data. -func (inst Finalize) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } +func (inst Finalize) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } func (inst *Finalize) UnmarshalWithDecoder(_ *ag_binary.Decoder) error { return nil } func NewFinalizeInstruction(account ag_solanago.PublicKey) *Finalize { diff --git a/programs/loader-v2/Write.go b/programs/loader-v2/Write.go index 7e18a02ee..ea3f85bed 100644 --- a/programs/loader-v2/Write.go +++ b/programs/loader-v2/Write.go @@ -29,7 +29,8 @@ import ( // Write program data into an account. // // Account references: -// [0] = [WRITE, SIGNER] Account to write to +// +// [0] = [WRITE, SIGNER] Account to write to type Write struct { Offset *uint32 Bytes []byte @@ -78,7 +79,7 @@ func (inst Write) ValidateAndBuild() (*Instruction, error) { func (inst *Write) Validate() error { if inst.Offset == nil { - return errors.New("Offset parameter is not set") + return errors.New("offset parameter is not set") } for i, acc := range inst.AccountMetaSlice { if acc == nil { @@ -109,7 +110,7 @@ func (inst *Write) EncodeToTree(parent ag_treeout.Branches) { // [offset: u32 LE][len(bytes): u64 LE][bytes...] // // ag_binary's default slice encoder uses UVarInt for the length, which does -// not match bincode, so Vec is serialised manually. +// not match bincode, so Vec is serialized manually. func (inst Write) MarshalWithEncoder(encoder *ag_binary.Encoder) error { if err := encoder.WriteUint32(*inst.Offset, binary.LittleEndian); err != nil { return err diff --git a/programs/loader-v2/instructions.go b/programs/loader-v2/instructions.go index f13512450..6b0458b95 100644 --- a/programs/loader-v2/instructions.go +++ b/programs/loader-v2/instructions.go @@ -81,8 +81,8 @@ func (inst *Instruction) EncodeToTree(parent ag_treeout.Branches) { var InstructionImplDef = ag_binary.NewVariantDefinition( ag_binary.Uint32TypeIDEncoding, []ag_binary.VariantType{ - {"Write", (*Write)(nil)}, - {"Finalize", (*Finalize)(nil)}, + {Name: "Write", Type: (*Write)(nil)}, + {Name: "Finalize", Type: (*Finalize)(nil)}, }, ) diff --git a/programs/loader-v3/Close.go b/programs/loader-v3/Close.go index fc6128b6b..1a4906c46 100644 --- a/programs/loader-v3/Close.go +++ b/programs/loader-v3/Close.go @@ -28,10 +28,11 @@ import ( // its lamports. // // Account references: -// [0] = [WRITE] Account to close -// [1] = [WRITE] Lamports recipient -// [2] = [SIGNER, optional] Authority (omit for uninitialized close) -// [3] = [WRITE, optional] Program (required when closing programdata) +// +// [0] = [WRITE] Account to close +// [1] = [WRITE] Lamports recipient +// [2] = [SIGNER, optional] Authority (omit for uninitialized close) +// [3] = [WRITE, optional] Program (required when closing programdata) // // Tombstone (SIMD-0432): when true, the closed account is left as a // tombstone rather than fully reclaimed, blocking future re-use of the diff --git a/programs/loader-v3/DeployWithMaxDataLen.go b/programs/loader-v3/DeployWithMaxDataLen.go index e7ee97d99..462865e59 100644 --- a/programs/loader-v3/DeployWithMaxDataLen.go +++ b/programs/loader-v3/DeployWithMaxDataLen.go @@ -30,14 +30,15 @@ import ( // reserving `MaxDataLen` bytes for future upgrades. // // Account references: -// [0] = [WRITE, SIGNER] Payer -// [1] = [WRITE] ProgramData (PDA) -// [2] = [WRITE] Program account -// [3] = [WRITE] Source buffer -// [4] = [] Rent sysvar -// [5] = [] Clock sysvar -// [6] = [] System program -// [7] = [SIGNER] Upgrade authority +// +// [0] = [WRITE, SIGNER] Payer +// [1] = [WRITE] ProgramData (PDA) +// [2] = [WRITE] Program account +// [3] = [WRITE] Source buffer +// [4] = [] Rent sysvar +// [5] = [] Clock sysvar +// [6] = [] System program +// [7] = [SIGNER] Upgrade authority // // CloseBuffer (SIMD-0430): when true, the source buffer is closed and its // lamports returned to the payer atomically with the deploy. diff --git a/programs/loader-v3/ExtendProgram.go b/programs/loader-v3/ExtendProgram.go index 2ed68bedd..fa45cc91d 100644 --- a/programs/loader-v3/ExtendProgram.go +++ b/programs/loader-v3/ExtendProgram.go @@ -29,10 +29,11 @@ import ( // (SIMD-0431 caps the minimum granularity at MINIMUM_EXTEND_PROGRAM_BYTES). // // Account references: -// [0] = [WRITE] ProgramData (PDA) -// [1] = [WRITE] Program account -// [2] = [optional] System program (required when payer is provided) -// [3] = [WRITE, SIGNER, optional] Payer (covers any additional rent) +// +// [0] = [WRITE] ProgramData (PDA) +// [1] = [WRITE] Program account +// [2] = [optional] System program (required when payer is provided) +// [3] = [WRITE, SIGNER, optional] Payer (covers any additional rent) type ExtendProgram struct { AdditionalBytes *uint32 diff --git a/programs/loader-v3/InitializeBuffer.go b/programs/loader-v3/InitializeBuffer.go index 22d65c3a7..a6264d1be 100644 --- a/programs/loader-v3/InitializeBuffer.go +++ b/programs/loader-v3/InitializeBuffer.go @@ -29,9 +29,10 @@ import ( // and records the authority that is permitted to write to it. // // Account references: -// [0] = [WRITE] Buffer account -// [1] = [] Authority (not a signer here; only used to record the -// authority address) +// +// [0] = [WRITE] Buffer account +// [1] = [] Authority (not a signer here; only used to record the +// authority address) type InitializeBuffer struct { ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` } @@ -88,7 +89,7 @@ func (inst *InitializeBuffer) EncodeToTree(parent ag_treeout.Branches) { }) } -func (inst InitializeBuffer) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } +func (inst InitializeBuffer) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } func (inst *InitializeBuffer) UnmarshalWithDecoder(_ *ag_binary.Decoder) error { return nil } // NewInitializeBufferInstruction builds the InitializeBuffer instruction diff --git a/programs/loader-v3/SetAuthority.go b/programs/loader-v3/SetAuthority.go index ad2e4e27b..673bb7376 100644 --- a/programs/loader-v3/SetAuthority.go +++ b/programs/loader-v3/SetAuthority.go @@ -25,19 +25,21 @@ import ( ) // SetAuthority changes the authority on a buffer or programdata account. -// The unchecked variant is deprecated upstream in favour of +// The unchecked variant is deprecated upstream in favor of // SetAuthorityChecked when setting a new non-nil authority, but it remains // the only way to clear an upgrade authority (pass nil for the new authority). // // Account references (buffer form): -// [0] = [WRITE] Buffer account -// [1] = [SIGNER] Current authority -// [2] = [optional] New authority (omit to drop the authority) +// +// [0] = [WRITE] Buffer account +// [1] = [SIGNER] Current authority +// [2] = [optional] New authority (omit to drop the authority) // // Account references (programdata form): -// [0] = [WRITE] ProgramData (PDA) -// [1] = [SIGNER] Current authority -// [2] = [optional] New authority (omit to make the program immutable) +// +// [0] = [WRITE] ProgramData (PDA) +// [1] = [SIGNER] Current authority +// [2] = [optional] New authority (omit to make the program immutable) type SetAuthority struct { ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` } @@ -86,11 +88,11 @@ func (inst *SetAuthority) EncodeToTree(parent ag_treeout.Branches) { }) } -func (inst SetAuthority) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } +func (inst SetAuthority) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } func (inst *SetAuthority) UnmarshalWithDecoder(_ *ag_binary.Decoder) error { return nil } // NewSetBufferAuthorityInstruction builds a SetAuthority that transfers a -// buffer's authority. Upstream deprecates this in favour of +// buffer's authority. Upstream deprecates this in favor of // NewSetBufferAuthorityCheckedInstruction, but it is retained for decoding // historical transactions. func NewSetBufferAuthorityInstruction( diff --git a/programs/loader-v3/SetAuthorityChecked.go b/programs/loader-v3/SetAuthorityChecked.go index 168c6059c..b5a2ae27c 100644 --- a/programs/loader-v3/SetAuthorityChecked.go +++ b/programs/loader-v3/SetAuthorityChecked.go @@ -28,9 +28,10 @@ import ( // must co-sign the transaction, preventing typos that would lock a program. // // Account references: -// [0] = [WRITE] Target account (buffer or programdata) -// [1] = [SIGNER] Current authority -// [2] = [SIGNER] New authority +// +// [0] = [WRITE] Target account (buffer or programdata) +// [1] = [SIGNER] Current authority +// [2] = [SIGNER] New authority type SetAuthorityChecked struct { ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` } @@ -78,7 +79,7 @@ func (inst *SetAuthorityChecked) EncodeToTree(parent ag_treeout.Branches) { }) } -func (inst SetAuthorityChecked) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } +func (inst SetAuthorityChecked) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } func (inst *SetAuthorityChecked) UnmarshalWithDecoder(_ *ag_binary.Decoder) error { return nil } // NewSetBufferAuthorityCheckedInstruction builds a SetAuthorityChecked that diff --git a/programs/loader-v3/Upgrade.go b/programs/loader-v3/Upgrade.go index 41caec44e..007c8c0bc 100644 --- a/programs/loader-v3/Upgrade.go +++ b/programs/loader-v3/Upgrade.go @@ -27,13 +27,14 @@ import ( // Upgrade replaces an existing program's code with the contents of a buffer. // // Account references: -// [0] = [WRITE] ProgramData (PDA) -// [1] = [WRITE] Program account -// [2] = [WRITE] Source buffer -// [3] = [WRITE] Spill (lamports recipient) -// [4] = [] Rent sysvar -// [5] = [] Clock sysvar -// [6] = [SIGNER] Upgrade authority +// +// [0] = [WRITE] ProgramData (PDA) +// [1] = [WRITE] Program account +// [2] = [WRITE] Source buffer +// [3] = [WRITE] Spill (lamports recipient) +// [4] = [] Rent sysvar +// [5] = [] Clock sysvar +// [6] = [SIGNER] Upgrade authority // // CloseBuffer (SIMD-0430): when true, the source buffer is closed and its // lamports sent to the spill account atomically with the upgrade. diff --git a/programs/loader-v3/Write.go b/programs/loader-v3/Write.go index 86bee92bd..0279321a6 100644 --- a/programs/loader-v3/Write.go +++ b/programs/loader-v3/Write.go @@ -29,8 +29,9 @@ import ( // Write copies a chunk of program bytes into a buffer account. // // Account references: -// [0] = [WRITE] Buffer account to write to -// [1] = [SIGNER] Buffer authority +// +// [0] = [WRITE] Buffer account to write to +// [1] = [SIGNER] Buffer authority type Write struct { Offset *uint32 Bytes []byte @@ -73,7 +74,7 @@ func (inst Write) ValidateAndBuild() (*Instruction, error) { func (inst *Write) Validate() error { if inst.Offset == nil { - return errors.New("Offset parameter is not set") + return errors.New("offset parameter is not set") } for i, acc := range inst.AccountMetaSlice { if acc == nil { diff --git a/programs/loader-v3/accounts_test.go b/programs/loader-v3/accounts_test.go index 6f23bf494..1bf14e226 100644 --- a/programs/loader-v3/accounts_test.go +++ b/programs/loader-v3/accounts_test.go @@ -230,10 +230,9 @@ func TestDeployWithMaxProgramLenInstructions(t *testing.T) { func TestPDA_GetProgramDataAddress(t *testing.T) { // Spot-check: derivation is stable for the same inputs and distinct // from the program address itself. - pda, bump, err := GetProgramDataAddress(testProgram) + pda, _, err := GetProgramDataAddress(testProgram) require.NoError(t, err) require.NotEqual(t, testProgram, pda) - require.True(t, bump <= 255) pda2, _, err := GetProgramDataAddress(testProgram) require.NoError(t, err) diff --git a/programs/loader-v3/instructions.go b/programs/loader-v3/instructions.go index eb5ea5a09..2c48a4422 100644 --- a/programs/loader-v3/instructions.go +++ b/programs/loader-v3/instructions.go @@ -110,14 +110,14 @@ func (inst *Instruction) EncodeToTree(parent ag_treeout.Branches) { var InstructionImplDef = ag_binary.NewVariantDefinition( ag_binary.Uint32TypeIDEncoding, []ag_binary.VariantType{ - {"InitializeBuffer", (*InitializeBuffer)(nil)}, - {"Write", (*Write)(nil)}, - {"DeployWithMaxDataLen", (*DeployWithMaxDataLen)(nil)}, - {"Upgrade", (*Upgrade)(nil)}, - {"SetAuthority", (*SetAuthority)(nil)}, - {"Close", (*Close)(nil)}, - {"ExtendProgram", (*ExtendProgram)(nil)}, - {"SetAuthorityChecked", (*SetAuthorityChecked)(nil)}, + {Name: "InitializeBuffer", Type: (*InitializeBuffer)(nil)}, + {Name: "Write", Type: (*Write)(nil)}, + {Name: "DeployWithMaxDataLen", Type: (*DeployWithMaxDataLen)(nil)}, + {Name: "Upgrade", Type: (*Upgrade)(nil)}, + {Name: "SetAuthority", Type: (*SetAuthority)(nil)}, + {Name: "Close", Type: (*Close)(nil)}, + {Name: "ExtendProgram", Type: (*ExtendProgram)(nil)}, + {Name: "SetAuthorityChecked", Type: (*SetAuthorityChecked)(nil)}, }, ) diff --git a/programs/loader-v3/state.go b/programs/loader-v3/state.go index 5b73e70a0..7f97836db 100644 --- a/programs/loader-v3/state.go +++ b/programs/loader-v3/state.go @@ -33,10 +33,10 @@ const ( // Size constants for the serialized UpgradeableLoaderState header, matching // upstream's `UpgradeableLoaderState::size_of_*` helpers. const ( - SizeOfUninitialized = 4 // u32 discriminant - SizeOfBufferMetadata = 37 // disc(4) + Option tag(1) + 32 - SizeOfProgram = 36 // disc(4) + Pubkey(32) - SizeOfProgramDataMetadata = 45 // disc(4) + slot(8) + Option tag(1) + 32 + SizeOfUninitialized = 4 // u32 discriminant + SizeOfBufferMetadata = 37 // disc(4) + Option tag(1) + 32 + SizeOfProgram = 36 // disc(4) + Pubkey(32) + SizeOfProgramDataMetadata = 45 // disc(4) + slot(8) + Option tag(1) + 32 ) // SizeOfBuffer returns the account size required to hold a Buffer of the diff --git a/programs/loader-v4/Copy.go b/programs/loader-v4/Copy.go index 8e7b28394..204509f0e 100644 --- a/programs/loader-v4/Copy.go +++ b/programs/loader-v4/Copy.go @@ -29,9 +29,10 @@ import ( // program account without having to round-trip through `Write`. // // Account references: -// [0] = [WRITE] Destination program -// [1] = [SIGNER] Authority -// [2] = [] Source program +// +// [0] = [WRITE] Destination program +// [1] = [SIGNER] Authority +// [2] = [] Source program type Copy struct { DestinationOffset *uint32 SourceOffset *uint32 @@ -87,7 +88,7 @@ func (inst *Copy) Validate() error { return errors.New("SourceOffset parameter is not set") } if inst.Length == nil { - return errors.New("Length parameter is not set") + return errors.New("length parameter is not set") } for i, acc := range inst.AccountMetaSlice { if acc == nil { diff --git a/programs/loader-v4/Deploy.go b/programs/loader-v4/Deploy.go index a6d4c075a..298a4d077 100644 --- a/programs/loader-v4/Deploy.go +++ b/programs/loader-v4/Deploy.go @@ -29,13 +29,15 @@ import ( // bytes from another program account instead of the target itself. // // Account references (two-account form): -// [0] = [WRITE] Program account -// [1] = [SIGNER] Authority +// +// [0] = [WRITE] Program account +// [1] = [SIGNER] Authority // // Account references (deploy-from-source form): -// [0] = [WRITE] Program account -// [1] = [SIGNER] Authority -// [2] = [WRITE] Source program account +// +// [0] = [WRITE] Program account +// [1] = [SIGNER] Authority +// [2] = [WRITE] Source program account type Deploy struct { ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` } @@ -84,7 +86,7 @@ func (inst *Deploy) EncodeToTree(parent ag_treeout.Branches) { }) } -func (inst Deploy) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } +func (inst Deploy) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } func (inst *Deploy) UnmarshalWithDecoder(_ *ag_binary.Decoder) error { return nil } // NewDeployInstruction builds the two-account form: deploy the program diff --git a/programs/loader-v4/Finalize.go b/programs/loader-v4/Finalize.go index e30b31a0b..f449bdaa8 100644 --- a/programs/loader-v4/Finalize.go +++ b/programs/loader-v4/Finalize.go @@ -30,9 +30,10 @@ import ( // pointer's semantics. // // Account references: -// [0] = [WRITE] Program account -// [1] = [SIGNER] Authority -// [2] = [] Next-version program account +// +// [0] = [WRITE] Program account +// [1] = [SIGNER] Authority +// [2] = [] Next-version program account type Finalize struct { ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` } @@ -95,7 +96,7 @@ func (inst *Finalize) EncodeToTree(parent ag_treeout.Branches) { }) } -func (inst Finalize) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } +func (inst Finalize) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } func (inst *Finalize) UnmarshalWithDecoder(_ *ag_binary.Decoder) error { return nil } func NewFinalizeInstruction( diff --git a/programs/loader-v4/Retract.go b/programs/loader-v4/Retract.go index 5d89c75d3..b8caa598f 100644 --- a/programs/loader-v4/Retract.go +++ b/programs/loader-v4/Retract.go @@ -27,8 +27,9 @@ import ( // Retract moves a Deployed program back to Retracted, re-enabling writes. // // Account references: -// [0] = [WRITE] Program account -// [1] = [SIGNER] Authority +// +// [0] = [WRITE] Program account +// [1] = [SIGNER] Authority type Retract struct { ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` } @@ -85,7 +86,7 @@ func (inst *Retract) EncodeToTree(parent ag_treeout.Branches) { }) } -func (inst Retract) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } +func (inst Retract) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } func (inst *Retract) UnmarshalWithDecoder(_ *ag_binary.Decoder) error { return nil } func NewRetractInstruction(program, authority ag_solanago.PublicKey) *Retract { diff --git a/programs/loader-v4/SetProgramLength.go b/programs/loader-v4/SetProgramLength.go index f0aa24020..a6bf358b3 100644 --- a/programs/loader-v4/SetProgramLength.go +++ b/programs/loader-v4/SetProgramLength.go @@ -30,9 +30,10 @@ import ( // effectively closes the account and refunds its rent to the recipient. // // Account references: -// [0] = [WRITE] Program account -// [1] = [SIGNER] Authority -// [2] = [WRITE] Recipient (rent refund destination) +// +// [0] = [WRITE] Program account +// [1] = [SIGNER] Authority +// [2] = [WRITE] Recipient (rent refund destination) type SetProgramLength struct { NewSize *uint32 diff --git a/programs/loader-v4/TransferAuthority.go b/programs/loader-v4/TransferAuthority.go index e0f4b48d7..3d3c56fa4 100644 --- a/programs/loader-v4/TransferAuthority.go +++ b/programs/loader-v4/TransferAuthority.go @@ -28,9 +28,10 @@ import ( // and new authorities must sign (no unchecked variant exists for v4). // // Account references: -// [0] = [WRITE] Program account -// [1] = [SIGNER] Current authority -// [2] = [SIGNER] New authority +// +// [0] = [WRITE] Program account +// [1] = [SIGNER] Current authority +// [2] = [SIGNER] New authority type TransferAuthority struct { ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` } @@ -93,7 +94,7 @@ func (inst *TransferAuthority) EncodeToTree(parent ag_treeout.Branches) { }) } -func (inst TransferAuthority) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } +func (inst TransferAuthority) MarshalWithEncoder(_ *ag_binary.Encoder) error { return nil } func (inst *TransferAuthority) UnmarshalWithDecoder(_ *ag_binary.Decoder) error { return nil } func NewTransferAuthorityInstruction(program, authority, newAuthority ag_solanago.PublicKey) *TransferAuthority { diff --git a/programs/loader-v4/Write.go b/programs/loader-v4/Write.go index dc3535a3a..f7c396abf 100644 --- a/programs/loader-v4/Write.go +++ b/programs/loader-v4/Write.go @@ -29,8 +29,9 @@ import ( // Write copies a chunk of bytes into a program account. // // Account references: -// [0] = [WRITE] Program account -// [1] = [SIGNER] Authority +// +// [0] = [WRITE] Program account +// [1] = [SIGNER] Authority type Write struct { Offset *uint32 Bytes []byte @@ -73,7 +74,7 @@ func (inst Write) ValidateAndBuild() (*Instruction, error) { func (inst *Write) Validate() error { if inst.Offset == nil { - return errors.New("Offset parameter is not set") + return errors.New("offset parameter is not set") } for i, acc := range inst.AccountMetaSlice { if acc == nil { diff --git a/programs/loader-v4/instructions.go b/programs/loader-v4/instructions.go index 391db8e8f..7cac8e240 100644 --- a/programs/loader-v4/instructions.go +++ b/programs/loader-v4/instructions.go @@ -96,13 +96,13 @@ func (inst *Instruction) EncodeToTree(parent ag_treeout.Branches) { var InstructionImplDef = ag_binary.NewVariantDefinition( ag_binary.Uint32TypeIDEncoding, []ag_binary.VariantType{ - {"Write", (*Write)(nil)}, - {"Copy", (*Copy)(nil)}, - {"SetProgramLength", (*SetProgramLength)(nil)}, - {"Deploy", (*Deploy)(nil)}, - {"Retract", (*Retract)(nil)}, - {"TransferAuthority", (*TransferAuthority)(nil)}, - {"Finalize", (*Finalize)(nil)}, + {Name: "Write", Type: (*Write)(nil)}, + {Name: "Copy", Type: (*Copy)(nil)}, + {Name: "SetProgramLength", Type: (*SetProgramLength)(nil)}, + {Name: "Deploy", Type: (*Deploy)(nil)}, + {Name: "Retract", Type: (*Retract)(nil)}, + {Name: "TransferAuthority", Type: (*TransferAuthority)(nil)}, + {Name: "Finalize", Type: (*Finalize)(nil)}, }, ) From 79f657a90cc5d367e5b30f096de5a20c1e9f3003 Mon Sep 17 00:00:00 2001 From: Sonic Date: Thu, 30 Apr 2026 01:03:41 +0300 Subject: [PATCH 4/4] fix: linter errors --- programs/bpf-loader/loader.go | 226 ++++++++++++++++++++++++++++++++++ rpc/jsonrpc/jsonrpc.go | 11 +- rpc/jsonrpc/jsonrpc_test.go | 16 +-- rpc/util_test.go | 20 +-- transaction_bench_test.go | 7 +- 5 files changed, 254 insertions(+), 26 deletions(-) create mode 100644 programs/bpf-loader/loader.go diff --git a/programs/bpf-loader/loader.go b/programs/bpf-loader/loader.go new file mode 100644 index 000000000..1c2c09367 --- /dev/null +++ b/programs/bpf-loader/loader.go @@ -0,0 +1,226 @@ +package bpfloader + +import ( + "encoding/binary" + "fmt" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/programs/system" + "github.com/gagliardetto/solana-go/rpc" +) + +const ( + PACKET_DATA_SIZE int = 1280 - 40 - 8 +) + +// https://github.com/solana-labs/solana/blob/v1.7.15/cli/src/program.rs#L1683 +func calculateMaxChunkSize( + createBuilder func(offset int, data []byte) *solana.TransactionBuilder, +) (size int, err error) { + transaction, err := createBuilder(0, []byte{}).Build() + if err != nil { + return + } + signatures := make( + []solana.Signature, + transaction.Message.Header.NumRequiredSignatures, + ) + transaction.Signatures = append(transaction.Signatures, signatures...) + serialized, err := transaction.MarshalBinary() + if err != nil { + return + } + size = PACKET_DATA_SIZE - len(serialized) - 1 + return +} + +// https://github.com/solana-labs/solana/blob/v1.7.15/cli/src/program.rs#L2006 +func completePartialProgramInit( + loaderId solana.PublicKey, + payerPubkey solana.PublicKey, + elfPubkey solana.PublicKey, + account *rpc.Account, + accountDataLen int, + minimumBalance uint64, + allowExcessiveBalance bool, +) (instructions []solana.Instruction, balanceNeeded uint64, err error) { + if account.Executable { + err = fmt.Errorf("buffer account is already executable") + return + } + if !account.Owner.Equals(loaderId) && + !account.Owner.Equals(solana.SystemProgramID) { + err = fmt.Errorf( + "buffer account passed is already in use by another program", + ) + return + } + if len(account.Data.GetBinary()) > 0 && + len(account.Data.GetBinary()) < accountDataLen { + err = fmt.Errorf( + "buffer account passed is not large enough, may have been for a " + + " different deploy?", + ) + return + } + + if len(account.Data.GetBinary()) == 0 && + account.Owner.Equals(solana.SystemProgramID) { + instructions = append( + instructions, + system.NewAllocateInstruction(uint64(accountDataLen), elfPubkey). + Build(), + ) + instructions = append( + instructions, + system.NewAssignInstruction(loaderId, elfPubkey).Build(), + ) + if account.Lamports < minimumBalance { + balance := minimumBalance - account.Lamports + instructions = append( + instructions, + system.NewTransferInstruction(balance, payerPubkey, elfPubkey). + Build(), + ) + balanceNeeded = balance + } else if account.Lamports > minimumBalance && + account.Owner.Equals(solana.SystemProgramID) && + !allowExcessiveBalance { + err = fmt.Errorf( + "buffer account has a balance: %v.%v; it may already be in use", + account.Lamports/solana.LAMPORTS_PER_SOL, + account.Lamports%solana.LAMPORTS_PER_SOL, + ) + return + } + } + return +} + +func load( + payerPubkey solana.PublicKey, + account *rpc.Account, + programData []byte, + bufferDataLen int, + minimumBalance uint64, + loaderId solana.PublicKey, + bufferPubkey solana.PublicKey, + allowExcessiveBalance bool, +) ( + initialBuilder *solana.TransactionBuilder, + writeBuilders []*solana.TransactionBuilder, + finalBuilder *solana.TransactionBuilder, + balanceNeeded uint64, + err error, +) { + var instructions []solana.Instruction + if account != nil { + instructions, balanceNeeded, err = completePartialProgramInit( + loaderId, + payerPubkey, + bufferPubkey, + account, + bufferDataLen, + minimumBalance, + allowExcessiveBalance, + ) + if err != nil { + return + } + } else { + instructions = append( + instructions, + system.NewCreateAccountInstruction( + minimumBalance, + uint64(bufferDataLen), + loaderId, + payerPubkey, + bufferPubkey, + ).Build(), + ) + balanceNeeded = minimumBalance + } + if len(instructions) > 0 { + initialBuilder = solana.NewTransactionBuilder().SetFeePayer(payerPubkey) + for _, instruction := range instructions { + initialBuilder = initialBuilder.AddInstruction(instruction) + } + } + + createBuilder := func(offset int, chunk []byte) *solana.TransactionBuilder { + data := make([]byte, len(chunk)+16) + binary.LittleEndian.PutUint32(data[0:], 0) + binary.LittleEndian.PutUint32(data[4:], uint32(offset)) + binary.LittleEndian.PutUint32(data[8:], uint32(len(chunk))) + binary.LittleEndian.PutUint32(data[12:], 0) + copy(data[16:], chunk) + instruction := solana.NewInstruction( + loaderId, + solana.AccountMetaSlice{ + solana.NewAccountMeta(bufferPubkey, true, true), + }, + data, + ) + return solana.NewTransactionBuilder(). + AddInstruction(instruction). + SetFeePayer(payerPubkey) + } + + chunkSize, err := calculateMaxChunkSize(createBuilder) + if err != nil { + return + } + writeBuilders = []*solana.TransactionBuilder{} + for i := 0; i < len(programData); i += chunkSize { + end := i + chunkSize + if end > len(programData) { + end = len(programData) + } + writeBuilders = append( + writeBuilders, + createBuilder(i, programData[i:end]), + ) + } + + finalBuilder = solana.NewTransactionBuilder().SetFeePayer(payerPubkey) + { + data := make([]byte, 4) + binary.LittleEndian.PutUint32(data[0:], 1) + instruction := solana.NewInstruction( + loaderId, + solana.AccountMetaSlice{ + solana.NewAccountMeta(bufferPubkey, true, true), + }, + data, + ) + finalBuilder.AddInstruction(instruction) + } + return +} + +func Deploy( + payerPubkey solana.PublicKey, + account *rpc.Account, + programData []byte, + minimumBalance uint64, + loaderId solana.PublicKey, + bufferPubkey solana.PublicKey, + allowExcessiveBalance bool, +) ( + initialBuilder *solana.TransactionBuilder, + writeBuilders []*solana.TransactionBuilder, + finalBuilder *solana.TransactionBuilder, + balanceNeeded uint64, + err error, +) { + return load( + payerPubkey, + account, + programData, + len(programData), + minimumBalance, + loaderId, + bufferPubkey, + allowExcessiveBalance, + ) +} diff --git a/rpc/jsonrpc/jsonrpc.go b/rpc/jsonrpc/jsonrpc.go index 98feaf454..0550206ee 100644 --- a/rpc/jsonrpc/jsonrpc.go +++ b/rpc/jsonrpc/jsonrpc.go @@ -12,7 +12,6 @@ import ( "sync/atomic" "github.com/davecgh/go-spew/spew" - stdjson "github.com/goccy/go-json" gojson "github.com/goccy/go-json" "github.com/google/uuid" ) @@ -205,10 +204,10 @@ func NewRequest(method string, params ...any) *RPCRequest { // // See: http://www.jsonrpc.org/specification#response_object type RPCResponse struct { - JSONRPC string `json:"jsonrpc"` - Result stdjson.RawMessage `json:"result,omitempty"` - Error *RPCError `json:"error,omitempty"` - ID any `json:"id"` + JSONRPC string `json:"jsonrpc"` + Result gojson.RawMessage `json:"result,omitempty"` + Error *RPCError `json:"error,omitempty"` + ID any `json:"id"` } // RPCError represents a JSON-RPC error object if an RPC error occurred. @@ -293,7 +292,7 @@ func (res RPCResponses) AsMap() map[any]*RPCResponse { for _, r := range res { actualID := r.ID if actualID != nil { - if asFloat, ok := actualID.(stdjson.Number); ok { + if asFloat, ok := actualID.(gojson.Number); ok { asInt64, err := asFloat.Int64() if err == nil { actualID = int(asInt64) diff --git a/rpc/jsonrpc/jsonrpc_test.go b/rpc/jsonrpc/jsonrpc_test.go index 1c7fee8a9..ec71b89f0 100644 --- a/rpc/jsonrpc/jsonrpc_test.go +++ b/rpc/jsonrpc/jsonrpc_test.go @@ -10,7 +10,7 @@ import ( "strconv" "testing" - stdjson "github.com/goccy/go-json" + gojson "github.com/goccy/go-json" . "github.com/onsi/gomega" "github.com/stretchr/testify/require" ) @@ -386,14 +386,14 @@ func TestRpcJsonResponseStruct(t *testing.T) { res, err = rpcClient.Call(context.Background(), "something", 1, 2, 3) <-requestChan Expect(err).To(BeNil()) - Expect(res.Result).To(Equal(stdjson.RawMessage([]byte(strconv.Quote("ok"))))) + Expect(res.Result).To(Equal(gojson.RawMessage([]byte(strconv.Quote("ok"))))) // result with error null is ok responseBody = `{"result": "ok", "error": null}` res, err = rpcClient.Call(context.Background(), "something", 1, 2, 3) <-requestChan Expect(err).To(BeNil()) - Expect(res.Result).To(Equal(stdjson.RawMessage([]byte(strconv.Quote("ok"))))) + Expect(res.Result).To(Equal(gojson.RawMessage([]byte(strconv.Quote("ok"))))) // error with result null is ok responseBody = `{"error": {"code": 123, "message": "something wrong"}, "result": null}` @@ -655,8 +655,8 @@ func TestRpcBatchJsonResponseStruct(t *testing.T) { }) <-requestChan Expect(err).To(BeNil()) - Expect(res[0].Result).To(Equal(stdjson.RawMessage([]byte(strconv.Quote("ok"))))) - Expect(res[0].ID).To(Equal(stdjson.Number("1"))) + Expect(res[0].Result).To(Equal(gojson.RawMessage([]byte(strconv.Quote("ok"))))) + Expect(res[0].ID).To(Equal(gojson.Number("1"))) // result with error null is ok responseBody = `[{"result": "ok", "error": null}]` @@ -665,7 +665,7 @@ func TestRpcBatchJsonResponseStruct(t *testing.T) { }) <-requestChan Expect(err).To(BeNil()) - Expect(res[0].Result).To(Equal(stdjson.RawMessage([]byte(strconv.Quote("ok"))))) + Expect(res[0].Result).To(Equal(gojson.RawMessage([]byte(strconv.Quote("ok"))))) // error with result null is ok responseBody = `[{"error": {"code": 123, "message": "something wrong"}, "result": null}]` @@ -731,10 +731,10 @@ func TestRpcBatchJsonResponseStruct(t *testing.T) { Expect(err).To(BeNil()) Expect(res[0].Error).To(BeNil()) - Expect(res[0].ID).To(Equal(stdjson.Number("1"))) + Expect(res[0].ID).To(Equal(gojson.Number("1"))) Expect(res[1].Error).To(BeNil()) - Expect(res[1].ID).To(Equal(stdjson.Number("2"))) + Expect(res[1].ID).To(Equal(gojson.Number("2"))) err = res[0].GetObject(&p) require.NoError(t, err) diff --git a/rpc/util_test.go b/rpc/util_test.go index 64a079868..a3d5d9349 100644 --- a/rpc/util_test.go +++ b/rpc/util_test.go @@ -11,19 +11,21 @@ import ( // Layout references for the test data below: // // SPL Token (solana-program/token): -// Mint::LEN = 82 -// Account::LEN = 165 +// +// Mint::LEN = 82 +// Account::LEN = 165 // // Token-2022 (solana-program/token-2022): -// Extended records place a 1-byte AccountType discriminator at offset -// Account::LEN (= 165). Mint base (82 bytes) is padded with 83 zeros so -// Mint and Account share the discriminator offset. // -// AccountType::Uninitialized = 0 -// AccountType::Mint = 1 -// AccountType::Account = 2 +// Extended records place a 1-byte AccountType discriminator at offset +// Account::LEN (= 165). Mint base (82 bytes) is padded with 83 zeros so +// Mint and Account share the discriminator offset. +// +// AccountType::Uninitialized = 0 +// AccountType::Mint = 1 +// AccountType::Account = 2 // -// Extensions follow as TLV: [u16 LE type][u16 LE length][value...]. +// Extensions follow as TLV: [u16 LE type][u16 LE length][value...]. const ( testAccountTypeUninitialized uint8 = 0 testAccountTypeMint uint8 = 1 diff --git a/transaction_bench_test.go b/transaction_bench_test.go index f14cdd686..99dbd2f35 100644 --- a/transaction_bench_test.go +++ b/transaction_bench_test.go @@ -11,7 +11,8 @@ import ( // // numInstructions: how many instructions in the transaction. // accountsPerIx: how many AccountMeta entries each instruction references. -// (first is always a signer; second is always writable; rest readonly) +// +// (first is always a signer; second is always writable; rest readonly) func buildBenchInstructions(numInstructions, accountsPerIx int) ([]Instruction, Hash) { // Pre-generate a pool of unique accounts so instructions share some accounts // (realistic — the same fee payer / writable state account appears in many ixs) @@ -98,9 +99,9 @@ func buildBenchInstructionsWithLookups(numInstructions, accountsPerIx int) ([]In // Upper bound is ~10 instructions / ~30 accounts per ix — beyond that // becomes a synthetic stress shape that doesn't represent real traffic. var benchTxShapes = []struct { - name string + name string numInstructions int - accountsPerIx int + accountsPerIx int }{ {"small_2ix_5accts", 2, 5}, {"medium_5ix_15accts", 5, 15},