Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support plugins #2051

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ After following these steps, Juno should be up and running on your machine, util
- Starknet state construction and storage using a path-based Merkle Patricia trie.
- Feeder gateway synchronisation of Blocks, Transactions, Receipts, State Updates and Classes.
- Block and Transaction hash verification.
- Plugins

## 🛣 Roadmap

Expand Down
18 changes: 17 additions & 1 deletion blockchain/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,23 @@
return b.database.Update(b.revertHead)
}

func (b *Blockchain) GetReverseStateDiff() (*core.StateDiff, error) {
var reverseStateDiff *core.StateDiff
return reverseStateDiff, b.database.View(func(txn db.Transaction) error {
blockNumber, err := chainHeight(txn)
if err != nil {
return err

Check warning on line 836 in blockchain/blockchain.go

View check run for this annotation

Codecov / codecov/patch

blockchain/blockchain.go#L836

Added line #L836 was not covered by tests
}
stateUpdate, err := stateUpdateByNumber(txn, blockNumber)
if err != nil {
return err

Check warning on line 840 in blockchain/blockchain.go

View check run for this annotation

Codecov / codecov/patch

blockchain/blockchain.go#L840

Added line #L840 was not covered by tests
}
state := core.NewState(txn)
reverseStateDiff, err = state.GetReverseStateDiff(blockNumber, stateUpdate.StateDiff)
return err
})
}

func (b *Blockchain) revertHead(txn db.Transaction) error {
blockNumber, err := chainHeight(txn)
if err != nil {
Expand Down Expand Up @@ -874,7 +891,6 @@
}

// Revert chain height and pending.

if genesisBlock {
if err = txn.Delete(db.Pending.Key()); err != nil {
return err
Expand Down
4 changes: 4 additions & 0 deletions cmd/juno/juno.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const (
callMaxStepsF = "rpc-call-max-steps"
corsEnableF = "rpc-cors-enable"
versionedConstantsFileF = "versioned-constants-file"
pluginPathF = "plugin-path"

defaultConfig = ""
defaulHost = "localhost"
Expand Down Expand Up @@ -119,6 +120,7 @@ const (
defaultGwTimeout = 5 * time.Second
defaultCorsEnable = false
defaultVersionedConstantsFile = ""
defaultPluginPath = ""

configFlagUsage = "The YAML configuration file."
logLevelFlagUsage = "Options: trace, debug, info, warn, error."
Expand Down Expand Up @@ -170,6 +172,7 @@ const (
"The upper limit is 4 million steps, and any higher value will still be capped at 4 million."
corsEnableUsage = "Enable CORS on RPC endpoints"
versionedConstantsFileUsage = "Use custom versioned constants from provided file"
pluginPathUsage = "Path to the plugins .so file"
rianhughes marked this conversation as resolved.
Show resolved Hide resolved
)

var Version string
Expand Down Expand Up @@ -355,6 +358,7 @@ func NewCmd(config *node.Config, run func(*cobra.Command, []string) error) *cobr
junoCmd.Flags().Bool(corsEnableF, defaultCorsEnable, corsEnableUsage)
junoCmd.Flags().String(versionedConstantsFileF, defaultVersionedConstantsFile, versionedConstantsFileUsage)
junoCmd.MarkFlagsMutuallyExclusive(p2pFeederNodeF, p2pPeersF)
junoCmd.Flags().String(pluginPathF, defaultPluginPath, pluginPathUsage)

junoCmd.AddCommand(GenP2PKeyPair(), DBCmd(defaultDBPath))

Expand Down
62 changes: 43 additions & 19 deletions core/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,10 +540,14 @@
return fmt.Errorf("remove declared classes: %v", err)
}

// update contracts
reversedDiff, err := s.buildReverseDiff(blockNumber, update.StateDiff)
reversedDiff, err := s.GetReverseStateDiff(blockNumber, update.StateDiff)
if err != nil {
return fmt.Errorf("build reverse diff: %v", err)
return fmt.Errorf("error getting reverse state diff: %v", err)

Check warning on line 545 in core/state.go

View check run for this annotation

Codecov / codecov/patch

core/state.go#L545

Added line #L545 was not covered by tests
}

err = s.performStateDeletions(blockNumber, update.StateDiff)
if err != nil {
return fmt.Errorf("error performing state deletions: %v", err)

Check warning on line 550 in core/state.go

View check run for this annotation

Codecov / codecov/patch

core/state.go#L550

Added line #L550 was not covered by tests
}

stateTrie, storageCloser, err := s.storage()
Expand All @@ -559,19 +563,26 @@
return err
}

if err = s.purgeNoClassContracts(); err != nil {
return err

Check warning on line 567 in core/state.go

View check run for this annotation

Codecov / codecov/patch

core/state.go#L567

Added line #L567 was not covered by tests
}

// purge deployed contracts
for addr := range update.StateDiff.DeployedContracts {
if err = s.purgeContract(&addr); err != nil {
return fmt.Errorf("purge contract: %v", err)
}
}

return s.verifyStateUpdateRoot(update.OldRoot)
}

func (s *State) purgeNoClassContracts() error {
// purge noClassContracts
//
// As noClassContracts are not in StateDiff.DeployedContracts we can only purge them if their storage no longer exists.
// Updating contracts with reverse diff will eventually lead to the deletion of noClassContract's storage key from db. Thus,
// we can use the lack of key's existence as reason for purging noClassContracts.

for addr := range noClassContracts {
noClassC, err := NewContractUpdater(&addr, s.txn)
if err != nil {
Expand All @@ -592,8 +603,7 @@
}
}
}

return s.verifyStateUpdateRoot(update.OldRoot)
return nil
}

func (s *State) removeDeclaredClasses(blockNumber uint64, v0Classes []*felt.Felt, v1Classes map[felt.Felt]*felt.Felt) error {
Expand Down Expand Up @@ -657,7 +667,7 @@
return storageCloser()
}

func (s *State) buildReverseDiff(blockNumber uint64, diff *StateDiff) (*StateDiff, error) {
func (s *State) GetReverseStateDiff(blockNumber uint64, diff *StateDiff) (*StateDiff, error) {
reversed := *diff

// storage diffs
Expand All @@ -673,10 +683,6 @@
}
value = oldValue
}

if err := s.DeleteContractStorageLog(&addr, &key, blockNumber); err != nil {
return nil, err
}
reversedDiffs[key] = value
}
reversed.StorageDiffs[addr] = reversedDiffs
Expand All @@ -686,18 +692,13 @@
reversed.Nonces = make(map[felt.Felt]*felt.Felt, len(diff.Nonces))
for addr := range diff.Nonces {
oldNonce := &felt.Zero

if blockNumber > 0 {
var err error
oldNonce, err = s.ContractNonceAt(&addr, blockNumber-1)
if err != nil {
return nil, err
}
}

if err := s.DeleteContractNonceLog(&addr, blockNumber); err != nil {
return nil, err
}
reversed.Nonces[addr] = oldNonce
}

Expand All @@ -712,12 +713,35 @@
return nil, err
}
}
reversed.ReplacedClasses[addr] = classHash
}

return &reversed, nil
}

func (s *State) performStateDeletions(blockNumber uint64, diff *StateDiff) error {
// storage diffs
for addr, storageDiffs := range diff.StorageDiffs {
for key := range storageDiffs {
if err := s.DeleteContractStorageLog(&addr, &key, blockNumber); err != nil {
return err

Check warning on line 727 in core/state.go

View check run for this annotation

Codecov / codecov/patch

core/state.go#L727

Added line #L727 was not covered by tests
}
}
}

// nonces
for addr := range diff.Nonces {
if err := s.DeleteContractNonceLog(&addr, blockNumber); err != nil {
return err

Check warning on line 735 in core/state.go

View check run for this annotation

Codecov / codecov/patch

core/state.go#L735

Added line #L735 was not covered by tests
}
}

// replaced classes
for addr := range diff.ReplacedClasses {
if err := s.DeleteContractClassHashLog(&addr, blockNumber); err != nil {
return nil, err
return err

Check warning on line 742 in core/state.go

View check run for this annotation

Codecov / codecov/patch

core/state.go#L742

Added line #L742 was not covered by tests
}
reversed.ReplacedClasses[addr] = classHash
}

return &reversed, nil
return nil
}
87 changes: 87 additions & 0 deletions docs/docs/plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
title: Juno Plugins
---

## Overview

The Juno client now supports plugins that satisfy the `JunoPlugin` interface. This enhancement allows developers to extend and customize Juno's behavior by dynamically loading external plugins at runtime.

The `JunoPlugin` interface provides a structured way for plugins to interact with the blockchain by receiving notifications when new blocks are added or reverted. This ensures state consistency, especially during blockchain reorganizations, while abstracting away the complexity of implementing block syncing and revert logic.

## The JunoPlugin Interface

Your plugin must implement the `JunoPlugin` interface, which includes methods for initializing, shutting down, and handling new and reverted blocks.


```go
type JunoPlugin interface {
Init() error
Shutdown() error // Note: Currently this function will never be called.
NewBlock(block *core.Block, stateUpdate *core.StateUpdate, newClasses map[felt.Felt]core.Class) error
RevertBlock(from, to *BlockAndStateUpdate, reverseStateDiff *core.StateDiff) error
}
```


**Init**: Called when the plugin is initialized. This can be used to set up database connections or any other necessary resources.

**Shutdown**: Called when the plugin is shut down, though this function is currently never called by Juno. This can be used to clean up resources like database connections.

**NewBlock**: Triggered when a new block is synced by the Juno client. Juno will send the block, the corresponding state update, and any new classes. Importantly, Juno waits for the plugin to finish processing this function call before continuing. This ensures that the plugin completes its task before Juno proceeds with the blockchain sync.

**RevertBlock**: Called during a blockchain reorganization (reorg). Juno will invoke this method for each block that needs to be reverted. Similar to NewBlock, the client will wait for the plugin to finish handling the revert before moving on to the next block.

## Example plugin

Here is a basic example of a plugin that satisfies the `JunoPlugin` interface:

```go
//go:generate go build -buildmode=plugin -o ../../build/plugin.so ./example.go
type examplePlugin string

// Important: "JunoPluginInstance" needs to be exported for Juno to load the plugin correctly
var JunoPluginInstance examplePlugin

var _ junoplugin.JunoPlugin = (*examplePlugin)(nil)

func (p *examplePlugin) Init() error {
fmt.Println("ExamplePlugin initialized")
return nil
}

func (p *examplePlugin) Shutdown() error {
fmt.Println("ExamplePlugin shutdown")
return nil
}

func (p *examplePlugin) NewBlock(block *core.Block, stateUpdate *core.StateUpdate, newClasses map[felt.Felt]core.Class) error {
fmt.Println("ExamplePlugin NewBlock called")
return nil
}

func (p *examplePlugin) RevertBlock(from, to *junoplugin.BlockAndStateUpdate, reverseStateDiff *core.StateDiff) error {
fmt.Println("ExamplePlugin RevertBlock called")
return nil
}
```

The ```JunoPluginInstance``` variable must be exported for Juno to correctly load the plugin:
```var JunoPluginInstance examplePlugin```

We ensure the plugin implements the ```JunoPlugin``` interface, with the following line:
```var _ junoplugin.JunoPlugin = (*examplePlugin)(nil)```



# Building and Loading the Plugin

Once you have written your plugin, you can compile it into a shared object file (.so) using the following command:
```go build -buildmode=plugin -o ./plugin.so /path/to/your/plugin.go```

This command compiles the plugin into a shared object file (```plugin.so```), which can then be loaded by the Juno client using the ```--plugin-path``` flag.

# Running Juno with the plugin

Once your plugin has been compiled into a `.so` file, you can run Juno with your plugin by providing the `--plugin-path` flag. This flag tells Juno where to find and load your plugin at runtime.


1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/ethereum/go-ethereum v1.14.9
github.com/fxamacker/cbor/v2 v2.7.0
github.com/go-playground/validator/v10 v10.22.1
github.com/golang/protobuf v1.5.4
github.com/jinzhu/copier v0.4.0
github.com/libp2p/go-libp2p v0.36.2
github.com/libp2p/go-libp2p-kad-dht v0.26.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
Expand Down
Loading