NEU: Helius übernimmt Light Protocol
Solana-Smart-Contracts mit Pinocchio entwickeln
Blog/Entwicklung

So entwickelst du Solana-Programme mit Pinocchio

Führende Solana-EntwicklungsagenturExo Technologies auf XExo Technologies auf LinkedIn
Mitgründer, Exo TechnologiesTaylor Johnson auf XTaylor Johnson auf LinkedIn
12 Min. Lesezeit

Pinocchio ist eine hochoptimierte Bibliothek ohne Abhängigkeiten, mit der du native Solana-Programme entwickeln kannst. Pinocchio wurde von Anza entwickelt, dem Kernentwicklerteam hinter Solanas Agave-Client. 

Exo Tech ist eine führende Solana-Entwicklungsagentur und gehörte zu den ersten Anwendern von Pinocchio. In unseren Kundenprojekten haben wir mehrere produktive Programme mit Pinocchio entwickelt und fehlende Funktionen zum SDK beigetragen. 

Dieser Artikel zeigt ausführlich, wie du Programme mit Pinocchio entwickelst, und beleuchtet die Vorteile und Kompromisse. Er soll dir das nötige Wissen geben, um zu entscheiden, ob Pinocchio zu deinem Programm passt. Beachte jedoch, dass Pinocchio nicht einsteigerfreundlich ist, da die Bibliothek Optimierung über die Developer Experience stellt.

Was ist die Pinocchio-Bibliothek?

Die Pinocchio-Bibliothek ersetzt das Crate solana-program und optimiert die Programmausführung durch den umfassenden Einsatz von zero-copy-Typen. zero-copy bedeutet, dass Daten beim Lesen oder Schreiben nicht an eine separate Speicheradresse kopiert werden müssen. Das spart Rechenressourcen, bei Solana auch CUs genannt.

Die Bibliothek hat keine Abhängigkeiten und ist „no_std“. Rusts Crate std bietet gängige Möglichkeiten, auf Betriebssystemressourcen und eine Runtime zuzugreifen. Da die Solana Virtual Machine (SVM) jedoch selbst eine Runtime ist, entfällt dieser Overhead.

Warum ist Pinocchio performanter als solana-program?

Jedes Solana-Programm benötigt einen Entrypoint, den die Runtime zur Programmausführung aufruft. Die Bibliothek solana-program stellt das Makro entrypoint! bereit. Es deserialisiert die Programmeingabe, richtet einen Heap-Allocator ein und erstellt einen Panic-Handler.

Code
macro_rules! entrypoint {
    ($process_instruction:ident) => {
        /// # Safety
        #[no_mangle]
        pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
            let (program_id, accounts, instruction_data) =
                unsafe { $crate::entrypoint::deserialize(input) };
            match $process_instruction(&program_id, &accounts, &instruction_data) {
                Ok(()) => $crate::entrypoint::SUCCESS,
                Err(error) => error.into(),
            }
        }
        $crate::custom_heap_default!();
        $crate::custom_panic_default!();
    };
}

Pinocchio exportiert drei Entrypoint-Makros.

Wenn du von solana-program migrierst, funktioniert das Makro entrypoint! weitgehend gleich: Es deserialisiert die Programmeingabe und richtet den Allocator und den Handler ein. 

Die beiden anderen Makros entkoppeln den Entrypoint jedoch von der Einrichtung des Heap-Allocators und des Panic-Handlers. Dadurch kannst du diese Komponenten weglassen oder optimieren, bevor deine Programmlogik ausgeführt wird. 

program_entrypoint! deserialisiert die Programmeingabe ähnlich wie solana-program. lazy_program_entrypoint! umschließt dagegen lediglich den Eingabepuffer und überlässt die Verarbeitung dem Programm. So hast du mehr Kontrolle über den Rechenaufwand. 

Da diese Makros weder den Heap-Allocator noch den Panic-Handler einrichten, stellt die Pinocchio-Bibliothek Standardmakros dafür bereit.

Wenn ein Programm keinen Heap-Speicher benötigt, spart no_allocator! außerdem Compute Units (CUs), indem es keinen Speicher-Allocator einrichtet.

Wie deserialisieren Pinocchio-Entrypoints die Eingaben von Solana-Programmen anders?

Wir haben kurz erwähnt, dass die Entrypoints von solana-program und Pinocchio die Programmeingabe deserialisieren. Es ist jedoch wichtig, die Unterschiede zu verstehen, denn daraus ergeben sich die größten CU-Einsparungen. 

Auf den ersten Blick sehen die deserialisierten Eingaben, die an den Instruction-Handler des Programms übergeben werden, gleich aus:

Code
/// solana-program and pinocchio both look the same
process_instruction(
         program_id: &Pubkey,
         accounts: &[AccountInfo],
         instruction_data: &[u8],
     ) -> ProgramResult

Der entscheidende Unterschied liegt in der Implementierung von AccountInfo. 

Während solana-program Daten in eine AccountInfo-Struktur schreibt, der diese Daten gehören, ist Pinocchios AccountInfo-Struktur selbst nur ein Zeiger auf die zugrunde liegenden Eingabedaten des Accounts. Dadurch müssen weniger Daten kopiert werden, was viele CUs spart.

Wie hilft Pinocchio Entwicklern beim Optimieren von CUs?

Da der Instruction-Prozessor Referenzen auf Zeiger erhält, werden Entwickler bei der Arbeit mit Pinocchio feststellen, dass ihre Logik nur selten Eigentümerin der verarbeiteten Daten ist. 

Das zeigt sich deutlich beim Zugriff auf Werte in AccountInfo. Wenn du den öffentlichen Schlüssel des Accounts mit der Methode key() liest, erhältst du eine Referenz auf Pubkey. Dadurch ist es während der gesamten Programmausführung günstiger, Account-Informationen zu lesen und Account-Daten zu verändern.

Beispiel für CU-Optimierung mit Pinocchio: P-token

Ein hervorragendes Beispiel für weitere Optimierungen mit Zero-Copy ist das Programm p-token.

Dieses Programm soll das kanonische SPL Token Program ersetzen. Mit Pinocchio reduziert es die Anzahl der Compute Units pro Transaktion drastisch.

Du wirst schnell feststellen, dass der gesamte Zustand über Zeiger abgerufen wird.

Statt den Token-Account zu deserialisieren, werden die Daten aus AccountInfo geprüft und anschließend wird ein Zeiger zurückgegeben.

Auf jede Eigenschaft wird über eine Funktion zugegriffen. Alle Werte, die keine primitiven Datentypen sind, geben eine Referenz zurück und erhalten so Zero-Copy. 

Mehr darüber, warum dies die CU-Nutzung drastisch reduziert, erfährst du in diesem Artikel zur CU-Optimierung.

Pinocchio vs. Anchor

Anchor ist ein sehr beliebtes, meinungsstarkes Framework für die Entwicklung von Solana-Programmen. Es gilt als höher abstrahiert als Pinocchio, da es keine Logik enthält, die zugrunde liegende Strukturen wie AccountInfo direkt verfügbar macht.

Stattdessen nutzt Anchor das zuvor erwähnte Crate solana-program und stellt Traits und Makros bereit, die die Programmentwicklung vereinfachen. Anchor bietet Muster für Instruction-Discriminators und Logik zur Deserialisierung von Accounts. Die Deserialisierungslogik basiert auf Borsh. Da Borsh kein Zero-Copy unterstützt, müssen Daten an eine andere Speicheradresse kopiert werden. 

Anchor beschleunigt durch seinen Komfort zwar die Entwicklung von Solana-Programmen, verbraucht dafür aber mehr CUs.

Pinocchio hingegen ist eine Bibliothek, die solana-program ersetzt, wenn Entwickler die Rechenleistung präzise steuern müssen. Sie macht keinerlei Vorgaben und lässt dich das Programm nach deinen Anforderungen strukturieren. Jedes Pinocchio-Projekt kann völlig anders aufgebaut sein, während Anchor-Projekte klar definierte Strukturen haben. 

Die Pinocchio-Bibliothek kümmert sich weder um Client-Bindings noch um deren Implementierung. Anchor hingegen unterstützt die IDL-Generierung nativ. Die IDL kann clientseitig für die Interaktion mit dem Programm verwendet werden.

Wenn du Pinocchio verwendest, musst du eigene Bindings schreiben oder andere Tools wie Shank und Codama nutzen. Diese stellen wir weiter unten im Abschnitt Ergänzende Tools für die Entwicklung mit Pinocchio vor.

Pinocchio vs. Steel

Steel ist ein weiteres Framework zum Schreiben von Solana-Programmen. Steel basiert derzeit auf solana-program und stellt Makros, Funktionen und Muster bereit, mit denen du sichere und aussagekräftige Programme schreiben kannst.

Durch seine klaren Vorgaben ist Steel leicht lesbar und bleibt dennoch modular. Anders als beim Alles-oder-nichts-Framework Anchor können Entwickler nur die Komponenten von Steel verwenden, die sie benötigen.

Das Makro account! von Steel verwendet bytemuck, um Account-Strukturen zu parsen. Pinocchio übernimmt das Parsen von Accounts dagegen überhaupt nicht. Steel bietet außerdem verkettbare Parser und Assertions, mit denen du einfach eigene Validierungen hinzufügen kannst. Pinocchio bringt solche Muster nicht mit. Entwickler müssen ihre Validierungsmuster selbst schreiben.

Bei gängigen Cross-Program Invocations (CPIs), etwa für das System Program und Token Program, stellen sowohl Pinocchio als auch Steel Muster bereit, die solche Aufrufe vereinfachen.

Pinocchio ist stark optimiert, überlässt aber jedes Detail dem Entwickler. Steel ist ein praktischer modularer Wrapper um die Bibliothek solana-program, der die Developer Experience verbessert.

So erstellst du mit Pinocchio einen Token

Um ein mit Pinocchio geschriebenes Programm zu demonstrieren, schreiben wir das Programm zum Erstellen von Tokens aus den Solana-Entwicklerbeispielen neu.

Dieses einfache Programm besitzt eine einzige Instruction. Sie erstellt einen Token2022-Token-Mint und speichert mithilfe der Metadata-Token-Erweiterung Informationen über den Token. Die Metadaten werden über Instruction-Daten bereitgestellt, die einen Namen, ein Symbol und einen URI enthalten.

1. Entrypoint definieren

Definieren wir zunächst den Entrypoint unseres Programms.

Wir verwenden das vollständige Entrypoint-Makro, da wir Pinocchios Standard-Allocator und Panic-Handling nutzen möchten.

Code
entrypoint!(process_instruction);

fn process_instruction(
   _program_id: &Pubkey,
   accounts: &[AccountInfo],
   instruction_data: &[u8],
) -> ProgramResult {
   Ok(())
}

2. Struktur der Instruction-Daten definieren

Als Nächstes definieren wir die Struktur unserer Instruction-Daten analog zu den anderen Beispielprogrammen. Um Entwicklungszeit zu sparen, verwenden wir Borsh für die Deserialisierung. Optimalere Deserialisierungsmethoden behandeln wir in einem anderen Artikel.

Code
#[derive(BorshDeserialize, Debug)]
pub struct CreateTokenArgs {
   pub name: String,
   pub symbol: String,
   pub uri: String,
   pub decimals: u8,
}

3. Accounts und Instruction-Daten parsen

Jetzt schreiben wir die Logik für unseren Instruction-Prozessor.

Zuerst müssen wir die Accounts aus der Account-Liste destrukturieren und die Instruction-Daten in unser CreateTokenArgs deserialisieren.

Code
let [mint_account, mint_authority, payer, token_program, _system_program] = accounts else {
       return Err(ProgramError::NotEnoughAccountKeys);
   };

   let args = CreateTokenArgs::try_from_slice(instruction_data)
       .map_err(|_| ProgramError::InvalidInstructionData)?;

4. Token2022-Mint-Account erstellen

Nachdem wir die Accounts und Instruction-Daten geparst haben, rufen wir die Instruction CreateAccount des System Program auf.

Im Folgenden verwenden wir die Struktur CreateAccount aus `pinocchio_system crate as it makes it very convenient to CPI by setting values of the struct and calling invoke.

Anders als bei einem normalen SPL-Token-Mint müssen wir den zusätzlichen Speicherplatz ermitteln, den die verwendeten Token-Erweiterungen benötigen.

Die Größe der Metadata-Pointer-Erweiterung ist statisch. Die Größe der Token-Metadata-Erweiterung muss dagegen dynamisch anhand der übergebenen Argumente berechnet werden.

Code
 /// [4 (extension discriminator) + 32 (update_authority) + 32 (metadata)]
   const METADATA_POINTER_SIZE: usize = 4 + 32 + 32;
   /// [4 (extension discriminator) + 32 (update_authority) + 32 (mint) + 4 (size of name ) + 4 (size of symbol) + 4 (size of uri) + 4 (size of additional_metadata)]
   const METADATA_EXTENSION_BASE_SIZE: usize = 4 + 32 + 32 + 4 + 4 + 4 + 4;
   /// Padding used so that Mint and Account extensions start at the same index
   const EXTENSIONS_PADDING_AND_OFFSET: usize = 84;

   /* within `process_instruction` */
   let extension_size = METADATA_POINTER_SIZE
       + METADATA_EXTENSION_BASE_SIZE
       + args.name.len()
       + args.symbol.len()
       + args.uri.len();
   let total_mint_size = Mint::LEN + EXTENSIONS_PADDING_AND_OFFSET + extension_size;

   let rent = Rent::get()?;
   // Create the account for the Mint
   CreateAccount {
       from: payer,
       to: mint_account,
       owner: token2022_program.key(),
       lamports: rent.minimum_balance(Mint::LEN),
       space: Mint::LEN as u64,
   }
   .invoke()?;

Nach dem Aufruf von CreateAccount hat das SystemProgram das Token2022-Programm als Eigentümer des Mint-Accounts registriert.

5. Erweiterung, Account und Metadatenwerte initialisieren

Als Nächstes müssen wir die Account-Daten setzen. Dazu initialisieren wir die Metadata-Pointer-Erweiterung und den Mint-Account mit dem Token2022-Programm sowie die Metadatenwerte, die unser Programm als Argumente erhalten hat. 

Die folgenden CPIs stammen aus einem aktiv entwickelten Branch des Crates pinocchio_token. Dieser Code wird daher wahrscheinlich bald veraltet sein, da die Token2022-Funktionalität aus dem SPL-Token-Crate ausgegliedert werden soll.

Code
// Initialize MetadataPointer extension pointing to the Mint account
   InitializeMetadataPointer {
       mint: mint_account,
       authority: Some(*payer.key()),
       metadata_address: Some(*mint_account.key()),
   }
   .invoke()?;

   // Now initialize that account as a Token2022 Mint
   InitializeMint2 {
       mint: mint_account,
       decimals: args.decimals,
       mint_authority: mint_authority.key(),
       freeze_authority: None,
   }
   .invoke(TokenProgramVariant::Token2022)?;

   // Set the metadata within the Mint account
   InitializeTokenMetadata {
       metadata: mint_account,
       update_authority: payer,
       mint: mint_account,
       mint_authority: payer,
       name: &args.name,
       symbol: &args.symbol,
       uri: &args.uri,
   }
   .invoke()?;

Das war's! 

Jetzt haben wir einen mit Pinocchio geschriebenen Token2022-Token-Mint mit eigenständigen Metadaten.

Dieser Code lässt sich noch weiter optimieren. Wir hoffen jedoch, dass er dir vermittelt, wie du Programme mit Pinocchio schreibst.

Ergänzende Tools für die Entwicklung mit Pinocchio

Es gibt bisher nur wenige spezielle Tools für Pinocchio, doch ihre Zahl wächst.

Bytemuck zum (De-)Serialisieren von Accounts

Entwickler eines Pinocchio-Programms müssen die (De-)Serialisierung von Accounts selbst implementieren. Manuell ist dieser Prozess mühsam und fehleranfällig. Bytemuck ist eine hervorragende Bibliothek, mit der du Byte-Arrays einfach als Strukturen lesen und schreiben kannst. Sie ist recht effizient, da sie die Menge der in den Speicher zu kopierenden Daten begrenzt.

Borsh ist eine weitere Lösung für Accounts ohne feste Größe. Borsh benötigt jedoch mehr Rechenleistung und ist einer der Gründe, warum Entwickler Pinocchio statt Anchor wählen.

Shank zum Generieren von IDLs

Da Pinocchio eine Bibliothek ist, bietet es im Gegensatz zu Anchor keine integrierte IDL-Generierung. Eine IDL (Interface Definition Language) ist eine JSON-Datei, die die öffentliche Schnittstelle eines Solana-Programms definiert. Dazu gehören Instructions, Account-Strukturen und Fehlercodes. Sie ermöglicht standardisierte Interaktionen und vereinfacht die clientseitige Entwicklung.

Für die IDL-Generierung empfehlen wir Shank. Mit diesem Crate kannst du deinen Code sehr einfach annotieren und über eine CLI eine gültige IDL generieren. Wenn du das Makro ShankAccount zur Derive-Anweisung einer Struktur hinzufügst, kennzeichnest du sie als (de-)serialisierbaren Account. Nach dem Ausführen der Shank-CLI erscheint diese Struktur als typisierter Account in der IDL und kann anschließend für die Client-Generierung verwendet werden.

Ein weiteres wichtiges Makro für das Instruction-Enum des Programms ist ShankInstruction. Damit kannst du über ein #[account]-Attribut den Index und die Berechtigungen jedes Accounts in der Liste für die jeweilige Instruction angeben.

Weitere Informationen zu nützlichen Code-Annotationen, mit denen du einfach IDLs für Programme ohne Anchor generierst, findest du im Repository shank-macro.

Codama zum Generieren von Clients

Sobald du eine IDL hast, kannst du mit Codama einfach Clients generieren. Falls der generierte Code nicht zu deinen Anforderungen passt, musst du die Clients manuell schreiben.

Bei Exo Tech haben wir eine Pinocchio-Projektvorlage erstellt, mit der wir schnell Repositories für Solana-Programme aufsetzen können. Probiere sie gern aus und öffne einen Pull Request, wenn du Verbesserungen hast!

Die Zukunft von Pinocchio

Pinocchio soll solana-program direkt ersetzen, bietet aber noch nicht denselben Funktionsumfang. Einige Sysvars werden noch nicht unterstützt. Crates außerhalb des Kerns werden entweder noch nicht vollständig unterstützt oder existieren nicht. Das Crate des Pinocchio Token Program unterstützt beispielsweise keine mehreren Signer. Auch Token2022 wird noch nicht unterstützt, befindet sich aber in Entwicklung.

Ein wesentlicher Nachteil von Pinocchio besteht darin, dass alle für andere Solana-Programme entwickelten SDKs das Crate solana-program verwenden. Daher muss jedes SDK Eigentümer von AccountInfo oder der weitergegebenen Daten sein. Das erschwert die Interoperabilität mit einem in Pinocchio entwickelten Programm erheblich. 

Bei der Integration von Drittanbieterprogrammen musst du häufig für jede Instruction eigene CPI-Logik schreiben. Codegeneratoren wie Codama könnten dieses Problem irgendwann lösen, sind aber noch nicht so weit.

Pinocchio wird noch aktiv entwickelt und wurde nicht auditiert. Die Community arbeitet weiterhin daran, die übrigen Sysvars in das SDK aufzunehmen und die Unterstützung wichtiger SPL-Programme wie Token und Token2022 zu verbessern.

So trägst du zu Pinocchio bei

Es gibt viele leicht umsetzbare Möglichkeiten, zu Pinocchio beizutragen.

Es gibt offene Issues und bestehende Pull Requests, die zusätzliche Unterstützung brauchen. Beteilige dich an den Diskussionen oder öffne einfach einen Pull Request, den die Maintainer prüfen können!

Fazit

Pinocchio ist beim Schreiben von Solana-Programmen deutlich performanter als bisherige Lösungen. Entwickler erhalten mehr Kontrolle über den Entrypoint ihres Programms. Durch zero-copy beim Zugriff auf Programmeingaben können sie außerdem die CU-Nutzung reduzieren. Die Bibliothek ist jedoch noch neu und bietet noch nicht alle Funktionen. Zum Zeitpunkt der Veröffentlichung dieses Artikels wurde sie nicht auditiert. Setze sie daher mit Bedacht ein.

Wenn du Pinocchio in Betracht ziehst, solltest du die Kompromisse gegenüber anderen Bibliotheken und Frameworks sorgfältig abwägen.

Meinungsstarke Frameworks wie Anchor beschleunigen die Programmentwicklung und vereinfachen die Wartung. Sie sind daher eine hervorragende Wahl, wenn eine kurze Markteinführungszeit wichtig ist.

Sobald dein Produkt stabil ist und ein hohes Transaktionsvolumen verarbeitet, kann sich die Optimierung von Solana-Programmen mit einer Bibliothek wie Pinocchio besser eignen.

Weitere Ressourcen

Weitere Informationen findest du in Febos Vortrag auf der Solana Accelerate 2025 und in diesen Lernressourcen:

Helius abonnieren

Bleib bei der Solana-Entwicklung auf dem Laufenden und erhalte Updates, wenn wir neue Beiträge veröffentlichen

Vergrößertes Bild