Sui.

Beitrag

Teile dein Wissen.

article banner.
harry phan.
Apr 24, 2025
Artikel

Seelenbindende Logik und das ReturnReceipt-Muster

#Seelenbindende Logik und das RetourReceipt-Muster

Nun zu einem speziellen Entwurfsmuster, das den Parent-Child-Mechanismus verwendet:Seelengebundene Objekte. Ein Objekt, das an eine Seele gebunden ist, soll an einen bestimmten Besitzer gebunden sein und nicht dauerhaft übertragbar sein. Möglicherweise möchten Sie jedoch temporär jemandem innerhalb einer einzelnen Transaktion Zugriff darauf gewähren — beispielsweise um eine Operation daran auszuführen —, aber sicherstellen, dass es am Ende dieser Transaktion wieder zu Ihnen zurückkehrt. Dies wird mit dem sogenanntenReturnReceipt-Mustererreicht, einer Variante des „Hot Potato“ -Musters.

###Das Hot Potato-Muster in Sui

Das Muster „heiße Kartoffel“ ist nach dem Kinderspiel benannt — wenn dir eine heiße Kartoffel 🌡️🥔 gereicht wird, darfst du sie nicht festhalten, du musst sie schnell weitergeben. Im Sinne von Move ist eine heiße Kartoffel in der Regel ein Objekt oder eine Ressource, muss in derselben Transaktion verbraucht oder weitergegeben werden, andernfalls schlägt die Transaktion fehl. Dies wird häufig dadurch implementiert, dass das Objekt nicht fallen kann (Sie können es also nicht einfach ignorieren oder fallen lassen) und die Logik so gestalten, dass die einzige Möglichkeit, es loszuwerden, darin besteht, die erforderliche Aktion auszuführen (z. B. einen Kredit zurückzugeben).

Ein häufiger Anwendungsfall ist einBlitzkredit: Sie leihen sich bei einer Transaktion einige Coins (Sie erhalten ein Kreditobjekt plus einen „Schulden“ -Token). Wenn Sie die Transaktion nicht bis zum Ende der Transaktion zurückzahlen, können Sie sie nicht abschließen, da Sie immer noch den Schuldschein besitzen, den Sie nicht fallen lassen dürfen — was Sie quasi zwingt, das Darlehen zurückzuzahlen oder abzubrechen.

###Beispiel für ein seelengebundenes Objekt

Nehmen wir an, wir haben ein SoulBound-Objekt, für das wir immer die Adresse seines ursprünglichen Besitzers behalten möchten. Wir können erzwingen, dass, wenn jemand es „leiht“ (als untergeordnetes Objekt unter einem übergeordneten Objekt), er es in derselben Transaktion zurückgeben muss. Wie? Mit einem ReturnReceipt.

Unten finden Sie eine vereinfachte Version, die von Suis Dokumentationsbeispiel für seelengebundene Objekte inspiriert ist

module demo::soul_bound {
    use sui::transfer::{Self, Receiving};
    use sui::object::UID;
    use sui::transfer;

    const EWrongObject: u64 = 0;

    /// A soul-bound object that cannot be permanently transferred.
    /// It has only `key` ability (no `store`), to tighten transfer/receive rules [oai_citation_attribution:40‡docs.sui.io](https://docs.sui.io/concepts/transfers/transfer-to-object#:~:text=%2F%2F%2F%20This%20object%20has%20,id%3A%20UID%2C).
    public struct SoulBound has key {
        id: UID,
        data: u64
    }

    /// A receipt that proves a SoulBound object must be returned.
    /// No abilities: cannot be copied, dropped, or stored (implicitly).
    public struct ReturnReceipt {
        /// The object ID of the soul-bound object that must be returned.
        object_id: UID,
        /// The address (or object ID) it must be returned to (the original owner).
        return_to: address
    }

    /// Allows the owner of `parent` to retrieve their SoulBound child.
    /// Returns the SoulBound object *and* a ReturnReceipt that compels return.
    public fun take_soul_bound(parent: &mut UID, sb_ticket: Receiving<SoulBound>): (SoulBound, ReturnReceipt) {
        let sb = transfer::receive(parent, sb_ticket);  // receive SoulBound (only this module can do it, since SoulBound has no store) [oai_citation_attribution:41‡docs.sui.io](https://docs.sui.io/concepts/transfers/transfer-to-object#:~:text=%2F%2F%2F%20,to_address%28%29%2C%20object_id%3A%20object%3A%3Aid%28%26soul_bound%29%2C) 
        let receipt = ReturnReceipt {
            object_id: sb.id,
            return_to: parent.to_address()
        };
        (sb, receipt)
    }

    /// Return a SoulBound object using the ReturnReceipt.
    /// This must be called in the same transaction after take_soul_bound.
    public fun return_soul_bound(sb: SoulBound, receipt: ReturnReceipt) {
        // Verify the receipt matches this SoulBound object
        assert!(sb.id == receipt.object_id, EWrongObject);
        // Send the SoulBound object back to the original owner address
        transfer::transfer(sb, receipt.return_to);
        // (ReturnReceipt is consumed/destroyed here as function param)
    }
}

In diesem Design:

  • SoulBound ist ein Objekt, das nur Schlüssel enthält. Indem wir es nicht speichern, verhindern wir die Verwendung von public_transfer oder public_receive, was bedeutet, dass die einzige Möglichkeit, es zu übertragen oder zu empfangen, über die Funktionen des definierenden Moduls besteht (um sicherzustellen, dass unsere benutzerdefinierte Logik verwendet wird).
  • take_soul_bound (analog zu „get_object“ in der Dokumentation) ist eine Funktion, die der Besitzer aufrufen würde, um sein seelengebundenes Objekt aus einem übergeordneten Objekt zu entfernen. Sie ruft transfer: :receive auf, um das SoulBound-Objekt abzurufen. Da SoulBound keinen Speicher hat, ist dieser Aufruf nur hier in seinem Modul erlaubt (was in Ordnung ist). Anschließend wird eine ReturnReceipt-Struktur erstellt, die die ID des seelengebundenen Objekts und die Adresse enthält, zu der zurückgekehrt werden soll (die Adresse des Elternteils). Wir gebenbeiderdas Objekt und die Quittung zurück.
  • ReturnReceipt hat keine Funktionen zum Ablegen, Speichern oder Kopieren (wir haben keine deklariert, daher wird es als Ressource behandelt, die weder gelöscht noch dupliziert werden kann). Das bedeutet, dass die Transaktion etwas damit anfangen muss**; andernfalls bleibt es eine übrig gebliebene Ressource und die VM lässt die Transaktion nicht erfolgreich beenden. Im Wesentlichen ist der ReturnReceipt unser Hot Potato Token 🔥🥔.
  • Das einzig Gültige, was mit einem ReturnReceipt zu tun ist, ist, return_soul_bound (oder eine ähnlich benannte Funktion) in derselben Transaktion aufzurufen. In return_soul_bound überprüfen wir, ob die ID des SoulBound-Objekts mit dem Datensatz der Quittung übereinstimmt (um zu verhindern, dass jemand versucht, ein anderes Objekt mit einer falschen Quittung zurückzugeben). Dann rufen wir transfer: :transfer (sb, receipt.return_to) auf, wodurch das SoulBound-Objekt an die Adresse auf der Quittung (den ursprünglichen Besitzer) zurückgesendet wird. Dadurch wird das seelengebundene Objekt effektiv an seinen rechtmäßigen Platz zurückgebracht. Das ReturnReceipt wird als Argument verwendet (also zerstört).
  • Wenn der Benutzer versucht, die Transaktion abzuschließen, ohne return_soul_bound aufzurufen, würde er immer noch das ReturnReceipt (aus der take_soul_bound-Ausgabe) in der Hand halten. Da ReturnReceipt keinen Drop hat, weigert sich die Move-VM, die Transaktion abzuschließen (in Move können Sie eine Ressource nicht einfach verwerfen; sie muss irgendwo verwendet oder gespeichert werden). Die Transaktion würde abgebrochen oder als ungültig betrachtet werden, was bedeutet, dass der gesamte Vorgang rückgängig gemacht wird.Ergebnis: Sie können nicht einfach mit dem seelengebundenen Objekt davonlaufen; wenn Sie es nicht zurückgeben, schlägt das TX fehl und es bleibt beim übergeordneten Objekt.

Von dem Moment an, in dem du take_soul_bound aufrufst, bis zu dem Moment, in dem du return_soul_bound in der Transaktion aufrufst, steht dir der SoulBound Objektwert zur Verfügung — vermutlich um einige erlaubte Operationen damit durchzuführen (vielleicht um ihn zu lesen oder nach Bedarf zu verwenden). Aber dumussihn zurückgeben, bevor die Transaktion abgeschlossen ist, dank der Empfangsbestätigung.

Dieses Muster wird oft als „seelengebunden = kann seinen Besitzer nicht verlassen“ beschrieben, aber genauer gesagt, kann es innerhalb einer einzigen Transaktion als untergeordnetes Objekt verlassen, mit der Garantie, dass es zurückkommt. Es ist, als würde man ein Bibliotheksbuch ausleihen — man muss es zurückgeben, bevor die Bibliothek für diesen Tag schließt 😅.

Warum überträgst du es nicht einfach nie? Es gibt Szenarien, in denen die vorübergehende Übertragung eines Objekts auf ein anderes Objekt (oder einen anderen Kontext) sinnvoll ist. Ein Beispiel ist einShared Object-Kontext: Vielleicht wird das SoulBound-Objekt während einer Operation unter einem gemeinsamen Objekt gespeichert und muss vom ursprünglichen Besitzer entfernt werden. Eine andere Möglichkeit besteht darin, eine kontrollierte Komposition zu ermöglichen — vielleicht möchten Sie ein Modul auf Ihr Objekt anwenden lassen, indem Sie es dem Objekt dieses Moduls geben und es dann zurückholen.

Indem wir SoulBound nur mit Schlüsseln versehen haben, haben wir sichergestellt, dass kein externer public_receive es ohne die Mitwirkung unseres Moduls abrufen kann. Durch die Verwendung der Quittung erzwingen wir die Einhaltung der Rückgabebedingungen. Der Kommentar in Suis Beispielcode weist sogar darauf hin, dass das ReturnReceipt das Tauschen verhindert — wenn zwei seelengebundene Objekte in einer Transaktion herausgenommen wurden, trägt jede Quittung eine bestimmte Objekt-ID, sodass du sie nicht verwechseln und das falsche zum Cheaten zurückgeben kannst.

Verallgemeinerung der Idee: Das ReturnReceipt-Muster kann immer dann verwendet werden, wenn Sie erzwingen möchten, dass ein Objekt zurückgegeben wird. Bei Blitzkrediten wird ein ähnliches Konzept verwendet (Kredit-Token und Schulden-Token, die zurückgezahlt werden müssen). Immer wenn Sie ein invariantes „Objekt X muss bis zum Ende von tx wieder an Adresse Y landen“ haben, können Sie eine Quittungsressource erstellen, um es aufrechtzuerhalten.

###Kurzer Rückblick:

-Seelengebundene Objekte: Nur Schlüssel, nur durch die Logik ihres Moduls abgerufen. -ReturnReceipt: Eine Dummy-Ressource, die zusammen mit dem Objekt zurückgegeben wird, um sicherzustellen, dass es zurückgegeben wird. Da es sich nicht um einen Drop-Vorgang handelt, muss der Benutzer die Return-Funktion aufrufen, andernfalls schlägt es fehl. -Hot Potato: Wenn du einen ReturnReceipt in der Hand hältst, solltest du besser damit umgehen (das Objekt zurückgeben), bevor die „Musik stoppt“ (Transaktion endet). — Wenn das Objekt nicht zurückgegeben wird, wird die Transaktion nicht erfolgreich ausgeführt. Ihre Änderungen werden rückgängig gemacht, wodurch die Soul-Bound-Regel effektiv durchgesetzt wird.

Und damit haben wir die wichtigsten Konzepte der Eltern-Kind-Beziehungen in Sui Move behandelt!

#5. Projektstruktur und Teststrategie (GitHub-fähig)**

Um diese Konzepte zu konkretisieren, solltest du vielleicht ein Sui Move-Projekt einrichten und mit den Beispielen spielen. Hier ist ein Vorschlag für ein Projektlayout, das Module für die obigen Beispiele enthält. Sie können Komponententests kompilieren und ausführen oder die Sui-CLI verwenden, um mit ihnen zu interagieren.

parent_child_demo/
├── Move.toml
├── sources/
│   ├── toy_box.move        (Parent & child in same module example)
│   ├── parcel.move         (Child module for Parcel)
│   ├── warehouse.move      (Parent module for Warehouse, uses public_receive)
│   └── soul_bound.move     (SoulBound and ReturnReceipt module)
└── tests/
    └── parent_child_test.move   (Integration tests for the modules)

Move.toML: Stellen Sie sicher, dass Sie das Sui-Framework und alle Adressen für Ihr Paket angeben. Zum Beispiel:

[package]
name = "parent_child_demo"
version = "0.0.1"
dependencies = [ 
    { name = "Sui", git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework", rev = "devnet" }
]

[addresses]
demo = "0x0"
  • (Verwenden Sie eine korrekte Adresse anstelle von 0x0, falls Sui dies verlangt, oder eine benannte Adresse, die Sie bei der Veröffentlichung zuweisen. ) *) *

Im Quellverzeichnis:

-toy_box.movekönnte das Modul example: :toy_box aus Teil 2 mit Toy- und Box-Definitionen und der take_toy-Funktion enthalten. -parcel.moveundwarehouse.movewerden die Module aus Teil 3 enthalten. -soul_bound.movewird das Modul aus Teil 4 enthalten.

Jedes Modul sollte sui::... je nach Bedarf für Transfer, object, tx_context usw. verwenden, wie in den Beispielen gezeigt. Die tests/parent_child_test.move können dann diese Module importieren und Szenarien simulieren:

module demo::parent_child_test {
    use 0xYourAddr::toy_box;
    use 0xYourAddr::warehouse;
    use 0xYourAddr::parcel;
    use 0xYourAddr::soul_bound;
    use sui::tx_context::TxContext;

    #[test]
    fun test_toy_withdraw() {
        let ctx = TxContext::new(); // pseudo, context is usually provided
        let my_box = toy_box::Box { id: object::new(&mut ctx) };
        let my_toy = toy_box::Toy { id: object::new(&mut ctx), name: b"Ball".to_vec() };
        // Transfer toy into the box
        transfer::transfer(my_toy, my_box.id); 
        // Now simulate receiving it back
        let ticket = transfer::Receiving<toy_box::Toy>{ /*...*/ }; // In real test, use transfer::make_receiver or a transaction call
        let returned_toy = toy_box::take_toy(&mut my_box, ticket);
        assert!(returned_toy.name == b"Ball".to_vec());
    }

    // Additional tests for warehouse/parcel and soul_bound can be written similarly.
}

Das Obige ist eine konzeptionelle Abbildung. In der Praxis kann sich der Testkontext von Sui unterscheiden. Möglicherweise müssen Sie in Tests transfer: :public_transfer verwenden, wenn Sie zu einem Objekt übertragen (falls es sich außerhalb des Moduls befindet), und das Empfangen aus dem Transaktionskontext verwenden. Sui bietet eine transfer: :make_receiver (reine Testfunktion), die ein Receiving anhand einer ID und Version erzeugen kann, was bei Komponententests nützlich ist, um die Eingabe eines untergeordneten Objekts zu simulieren.

Tests ausführen: Sie können den Sui Move-Test ausführen, der die # [test] -Funktionen ausführt. Oder stellen Sie das Paket in einem lokalen Sui-Netzwerk bereit und rufen Sie die Eingabefunktionen über CLI oder ein SDK auf, um das Verhalten zu beobachten:

  • Erstellen Sie ein Paket, erstellen Sie ein Warehouse, rufen Sie transfer: :public_transfer per CLI auf, um das Paket in das Warehouse zu bringen, und rufen Sie dann withdraw_parcel auf.
  • Erstellen Sie ein SoulBound-Objekt (vielleicht indem Sie einen Eintrag unterhaltsam machen, um eines zu minten), übertragen Sie es an ein übergeordnetes Objekt (oder ein gemeinsames Objekt), rufen Sie dann in einer einzigen Transaktion take_soul_bound auf und lassen Sie return_soul_bound weg, um zu sehen, dass die Transaktion fehlschlägt (erwartet), anstatt den Rückruf einzubeziehen, um den Erfolg zu sehen.

Jeder Teil dieses Projekts befasst sich mit einem der Themen:

  • toy_box.move: Grundlegende Eltern-Kind- und Empfangsfunktion.
  • parcel.move & warehouse.move: modulübergreifende Kinder und public_receive.
  • soul_bound.move: seelengebunden mit ReturnReceipt.

Indem du mit diesen experimentierst, wirst du dein Verständnis von Suis Objektmodell vertiefen. Der Code ist so strukturiert, dass er lehrreich ist, und sollte nicht unverändert für die Produktion ohne Sicherheitsüberprüfungen verwendet werden, aber er bietet einen soliden Ausgangspunkt.


Fazit: Die Funktionen von Sui Move für übergeordnete und untergeordnete Objekte sind leistungsstark. Sie ermöglicht die Erstellung komplexer Datenstrukturen auf der Kette (wie Inventare, Wallets, Sammlungen) mit feingranularer Zugriffskontrolle. Die Kombination aus transfer: :receive/public_receive und dem Typsystem von Move stellt sicher, dass nur autorisierter Code untergeordnete Objekte abrufen kann, und Muster wie ReturnReceipt ermöglichen die Durchsetzung zeitlicher Eigentumsregeln (Seelenbindung, Flash Loans usw.). Wir haben ein paar lustige Analogien und Emojis eingestreut, aber am Ende des Tages sind dies robuste Tools für Baumeister. Jetzt mach weiter und baue etwas Magie mit verschachtelten Objekten! 🚀🔥

Wenn ein Objekt anstelle einer Kontoadresse auf ein anderes Objekt übertragen wird, bilden sie eineEltern-Kind-Beziehung. Du kannst dir das so vorstellen:

-Parent= Container-Objekt -Kind= Ding, das hineingelegt wurde

Move ist es egal, ob Sie auf ein Konto oder eine Objekt-ID übertragen. Es bewegt sich einfach*.

public struct Parent has key {
    id: UID,
    name: String,
}

public struct Child has key {
    id: UID,
    description: String,
}

##Eltern- und Kindobjekte erstellen

Das übergeordnete Element muss veränderbar sein und existieren. Du kannst keine Dinge in eine unveränderliche Box stopfen!

  • Sui
  • Architecture
4
Teilen
Kommentare
.

Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.

610Beiträge1335Antworten
Sui.X.Peera.

Verdiene deinen Anteil an 1000 Sui

Sammle Reputationspunkte und erhalte Belohnungen für deine Hilfe beim Wachstum der Sui-Community.

BelohnungskampagneJuli