diff --git a/message.go b/message.go index d7a382ee..b37a0ddb 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 diff --git a/program_ids.go b/program_ids.go index 77a55c9f..476e0fa4 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/loader-v2/Finalize.go b/programs/loader-v2/Finalize.go new file mode 100644 index 00000000..18e210ae --- /dev/null +++ b/programs/loader-v2/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 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 00000000..3cc9d6f1 --- /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 00000000..ea3f85be --- /dev/null +++ b/programs/loader-v2/Write.go @@ -0,0 +1,153 @@ +// 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 serialized 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 00000000..d526b807 --- /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 00000000..36e3acfb --- /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 00000000..6b0458b9 --- /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{ + {Name: "Write", Type: (*Write)(nil)}, + {Name: "Finalize", Type: (*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 00000000..f6c0739f --- /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 00000000..be3716f0 --- /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 00000000..1a4906c4 --- /dev/null +++ b/programs/loader-v3/Close.go @@ -0,0 +1,141 @@ +// 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 00000000..462865e5 --- /dev/null +++ b/programs/loader-v3/DeployWithMaxDataLen.go @@ -0,0 +1,223 @@ +// 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 00000000..fa45cc91 --- /dev/null +++ b/programs/loader-v3/ExtendProgram.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 ( + "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 00000000..a6264d1b --- /dev/null +++ b/programs/loader-v3/InitializeBuffer.go @@ -0,0 +1,122 @@ +// 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 00000000..673bb737 --- /dev/null +++ b/programs/loader-v3/SetAuthority.go @@ -0,0 +1,128 @@ +// 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 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) +// +// 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 favor 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 00000000..b5a2ae27 --- /dev/null +++ b/programs/loader-v3/SetAuthorityChecked.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 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 00000000..007c8c0b --- /dev/null +++ b/programs/loader-v3/Upgrade.go @@ -0,0 +1,160 @@ +// 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 00000000..0279321a --- /dev/null +++ b/programs/loader-v3/Write.go @@ -0,0 +1,144 @@ +// 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 00000000..1bf14e22 --- /dev/null +++ b/programs/loader-v3/accounts_test.go @@ -0,0 +1,240 @@ +// 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, _, err := GetProgramDataAddress(testProgram) + require.NoError(t, err) + require.NotEqual(t, testProgram, pda) + + 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 00000000..2b50079f --- /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 00000000..2c48a442 --- /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{ + {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)}, + }, +) + +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 00000000..a29b2a51 --- /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 00000000..0fbabefb --- /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 00000000..7f97836d --- /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 00000000..afacb468 --- /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 00000000..204509f0 --- /dev/null +++ b/programs/loader-v4/Copy.go @@ -0,0 +1,160 @@ +// 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 00000000..298a4d07 --- /dev/null +++ b/programs/loader-v4/Deploy.go @@ -0,0 +1,114 @@ +// 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 00000000..f449bdaa --- /dev/null +++ b/programs/loader-v4/Finalize.go @@ -0,0 +1,109 @@ +// 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 00000000..b8caa598 --- /dev/null +++ b/programs/loader-v4/Retract.go @@ -0,0 +1,96 @@ +// 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 00000000..a6bf358b --- /dev/null +++ b/programs/loader-v4/SetProgramLength.go @@ -0,0 +1,153 @@ +// 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 00000000..3d3c56fa --- /dev/null +++ b/programs/loader-v4/TransferAuthority.go @@ -0,0 +1,105 @@ +// 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 00000000..f7c396ab --- /dev/null +++ b/programs/loader-v4/Write.go @@ -0,0 +1,144 @@ +// 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 00000000..c07308e9 --- /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 00000000..411f9460 --- /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 00000000..7cac8e24 --- /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{ + {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)}, + }, +) + +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 00000000..eb83f793 --- /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 00000000..ed4e484e --- /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 00000000..5d4cf13b --- /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()) +} diff --git a/rpc/jsonrpc/jsonrpc.go b/rpc/jsonrpc/jsonrpc.go index 98feaf45..0550206e 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 1c7fee8a..ec71b89f 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 64a07986..a3d5d934 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 f14cdd68..99dbd2f3 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},