Skip to content

Commit b43c46f

Browse files
rianhughesrianLordGhostX
authored
Support plugins (#2051)
Co-authored-by: rian <[email protected]> Co-authored-by: LordGhostX <[email protected]>
1 parent ca006de commit b43c46f

File tree

14 files changed

+487
-30
lines changed

14 files changed

+487
-30
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ After following these steps, Juno should be up and running on your machine, util
228228
- Starknet state construction and storage using a path-based Merkle Patricia trie.
229229
- Feeder gateway synchronisation of Blocks, Transactions, Receipts, State Updates and Classes.
230230
- Block and Transaction hash verification.
231+
- Plugins
231232

232233
## 🛣 Roadmap
233234

blockchain/blockchain.go

+17-1
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,23 @@ func (b *Blockchain) RevertHead() error {
828828
return b.database.Update(b.revertHead)
829829
}
830830

831+
func (b *Blockchain) GetReverseStateDiff() (*core.StateDiff, error) {
832+
var reverseStateDiff *core.StateDiff
833+
return reverseStateDiff, b.database.View(func(txn db.Transaction) error {
834+
blockNumber, err := chainHeight(txn)
835+
if err != nil {
836+
return err
837+
}
838+
stateUpdate, err := stateUpdateByNumber(txn, blockNumber)
839+
if err != nil {
840+
return err
841+
}
842+
state := core.NewState(txn)
843+
reverseStateDiff, err = state.GetReverseStateDiff(blockNumber, stateUpdate.StateDiff)
844+
return err
845+
})
846+
}
847+
831848
func (b *Blockchain) revertHead(txn db.Transaction) error {
832849
blockNumber, err := chainHeight(txn)
833850
if err != nil {
@@ -874,7 +891,6 @@ func (b *Blockchain) revertHead(txn db.Transaction) error {
874891
}
875892

876893
// Revert chain height and pending.
877-
878894
if genesisBlock {
879895
if err = txn.Delete(db.Pending.Key()); err != nil {
880896
return err

cmd/juno/juno.go

+4
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ const (
8282
callMaxStepsF = "rpc-call-max-steps"
8383
corsEnableF = "rpc-cors-enable"
8484
versionedConstantsFileF = "versioned-constants-file"
85+
pluginPathF = "plugin-path"
8586

8687
defaultConfig = ""
8788
defaulHost = "localhost"
@@ -119,6 +120,7 @@ const (
119120
defaultGwTimeout = 5 * time.Second
120121
defaultCorsEnable = false
121122
defaultVersionedConstantsFile = ""
123+
defaultPluginPath = ""
122124

123125
configFlagUsage = "The YAML configuration file."
124126
logLevelFlagUsage = "Options: trace, debug, info, warn, error."
@@ -170,6 +172,7 @@ const (
170172
"The upper limit is 4 million steps, and any higher value will still be capped at 4 million."
171173
corsEnableUsage = "Enable CORS on RPC endpoints"
172174
versionedConstantsFileUsage = "Use custom versioned constants from provided file"
175+
pluginPathUsage = "Path to the plugin .so file"
173176
)
174177

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

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

core/state.go

+43-21
Original file line numberDiff line numberDiff line change
@@ -540,10 +540,14 @@ func (s *State) Revert(blockNumber uint64, update *StateUpdate) error {
540540
return fmt.Errorf("remove declared classes: %v", err)
541541
}
542542

543-
// update contracts
544-
reversedDiff, err := s.buildReverseDiff(blockNumber, update.StateDiff)
543+
reversedDiff, err := s.GetReverseStateDiff(blockNumber, update.StateDiff)
545544
if err != nil {
546-
return fmt.Errorf("build reverse diff: %v", err)
545+
return fmt.Errorf("error getting reverse state diff: %v", err)
546+
}
547+
548+
err = s.performStateDeletions(blockNumber, update.StateDiff)
549+
if err != nil {
550+
return fmt.Errorf("error performing state deletions: %v", err)
547551
}
548552

549553
stateTrie, storageCloser, err := s.storage()
@@ -566,12 +570,17 @@ func (s *State) Revert(blockNumber uint64, update *StateUpdate) error {
566570
}
567571
}
568572

569-
// purge noClassContracts
570-
//
573+
if err = s.purgeNoClassContracts(); err != nil {
574+
return err
575+
}
576+
577+
return s.verifyStateUpdateRoot(update.OldRoot)
578+
}
579+
580+
func (s *State) purgeNoClassContracts() error {
571581
// As noClassContracts are not in StateDiff.DeployedContracts we can only purge them if their storage no longer exists.
572582
// Updating contracts with reverse diff will eventually lead to the deletion of noClassContract's storage key from db. Thus,
573583
// we can use the lack of key's existence as reason for purging noClassContracts.
574-
575584
for addr := range noClassContracts {
576585
noClassC, err := NewContractUpdater(&addr, s.txn)
577586
if err != nil {
@@ -592,8 +601,7 @@ func (s *State) Revert(blockNumber uint64, update *StateUpdate) error {
592601
}
593602
}
594603
}
595-
596-
return s.verifyStateUpdateRoot(update.OldRoot)
604+
return nil
597605
}
598606

599607
func (s *State) removeDeclaredClasses(blockNumber uint64, v0Classes []*felt.Felt, v1Classes map[felt.Felt]*felt.Felt) error {
@@ -657,7 +665,7 @@ func (s *State) purgeContract(addr *felt.Felt) error {
657665
return storageCloser()
658666
}
659667

660-
func (s *State) buildReverseDiff(blockNumber uint64, diff *StateDiff) (*StateDiff, error) {
668+
func (s *State) GetReverseStateDiff(blockNumber uint64, diff *StateDiff) (*StateDiff, error) {
661669
reversed := *diff
662670

663671
// storage diffs
@@ -673,10 +681,6 @@ func (s *State) buildReverseDiff(blockNumber uint64, diff *StateDiff) (*StateDif
673681
}
674682
value = oldValue
675683
}
676-
677-
if err := s.DeleteContractStorageLog(&addr, &key, blockNumber); err != nil {
678-
return nil, err
679-
}
680684
reversedDiffs[key] = value
681685
}
682686
reversed.StorageDiffs[addr] = reversedDiffs
@@ -686,18 +690,13 @@ func (s *State) buildReverseDiff(blockNumber uint64, diff *StateDiff) (*StateDif
686690
reversed.Nonces = make(map[felt.Felt]*felt.Felt, len(diff.Nonces))
687691
for addr := range diff.Nonces {
688692
oldNonce := &felt.Zero
689-
690693
if blockNumber > 0 {
691694
var err error
692695
oldNonce, err = s.ContractNonceAt(&addr, blockNumber-1)
693696
if err != nil {
694697
return nil, err
695698
}
696699
}
697-
698-
if err := s.DeleteContractNonceLog(&addr, blockNumber); err != nil {
699-
return nil, err
700-
}
701700
reversed.Nonces[addr] = oldNonce
702701
}
703702

@@ -712,12 +711,35 @@ func (s *State) buildReverseDiff(blockNumber uint64, diff *StateDiff) (*StateDif
712711
return nil, err
713712
}
714713
}
714+
reversed.ReplacedClasses[addr] = classHash
715+
}
715716

717+
return &reversed, nil
718+
}
719+
720+
func (s *State) performStateDeletions(blockNumber uint64, diff *StateDiff) error {
721+
// storage diffs
722+
for addr, storageDiffs := range diff.StorageDiffs {
723+
for key := range storageDiffs {
724+
if err := s.DeleteContractStorageLog(&addr, &key, blockNumber); err != nil {
725+
return err
726+
}
727+
}
728+
}
729+
730+
// nonces
731+
for addr := range diff.Nonces {
732+
if err := s.DeleteContractNonceLog(&addr, blockNumber); err != nil {
733+
return err
734+
}
735+
}
736+
737+
// replaced classes
738+
for addr := range diff.ReplacedClasses {
716739
if err := s.DeleteContractClassHashLog(&addr, blockNumber); err != nil {
717-
return nil, err
740+
return err
718741
}
719-
reversed.ReplacedClasses[addr] = classHash
720742
}
721743

722-
return &reversed, nil
744+
return nil
723745
}

docs/docs/plugins.md

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
title: Juno Plugins
3+
---
4+
5+
Juno supports plugins that satisfy the `JunoPlugin` interface, enabling developers to extend and customize Juno's behaviour and functionality by dynamically loading external plugins during runtime.
6+
7+
The `JunoPlugin` interface provides a structured way for plugins to interact with the blockchain by sending 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.
8+
9+
## JunoPlugin Interface
10+
11+
Your plugin must implement the `JunoPlugin` interface, which includes methods for initializing, shutting down, and handling new and reverted blocks.
12+
13+
```go
14+
type JunoPlugin interface {
15+
Init() error
16+
Shutdown() error
17+
NewBlock(block *core.Block, stateUpdate *core.StateUpdate, newClasses map[felt.Felt]core.Class) error
18+
RevertBlock(from, to *BlockAndStateUpdate, reverseStateDiff *core.StateDiff) error
19+
}
20+
```
21+
22+
**Init**: Called when the plugin is initialized. This can be used to set up database connections or any other necessary resources.
23+
24+
**Shutdown**: Called when the Juno node is shut down. This can be used to clean up resources like database connections.
25+
26+
**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.
27+
28+
**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.
29+
30+
## Example plugin
31+
32+
Here is a basic example of a plugin that satisfies the `JunoPlugin` interface:
33+
34+
```go
35+
// go:generate go build -buildmode=plugin -o ../../build/plugin.so ./example.go
36+
type examplePlugin string
37+
38+
// Important: "JunoPluginInstance" needs to be exported for Juno to load the plugin correctly
39+
var JunoPluginInstance examplePlugin
40+
41+
var _ junoplugin.JunoPlugin = (*examplePlugin)(nil)
42+
43+
func (p *examplePlugin) Init() error {
44+
fmt.Println("ExamplePlugin initialized")
45+
return nil
46+
}
47+
48+
func (p *examplePlugin) Shutdown() error {
49+
fmt.Println("ExamplePlugin shutdown")
50+
return nil
51+
}
52+
53+
func (p *examplePlugin) NewBlock(block *core.Block, stateUpdate *core.StateUpdate, newClasses map[felt.Felt]core.Class) error {
54+
fmt.Println("ExamplePlugin NewBlock called")
55+
return nil
56+
}
57+
58+
func (p *examplePlugin) RevertBlock(from, to *junoplugin.BlockAndStateUpdate, reverseStateDiff *core.StateDiff) error {
59+
fmt.Println("ExamplePlugin RevertBlock called")
60+
return nil
61+
}
62+
```
63+
64+
The `JunoPluginInstance` variable must be exported for Juno to correctly load the plugin:
65+
`var JunoPluginInstance examplePlugin`
66+
67+
We ensure the plugin implements the `JunoPlugin` interface, with the following line:
68+
`var _ junoplugin.JunoPlugin = (*examplePlugin)(nil)`
69+
70+
## Building and loading the plugin
71+
72+
Once you have written your plugin, you can compile it into a shared object file (.so) using the following command:
73+
74+
```shell
75+
go build -buildmode=plugin -o ./plugin.so /path/to/your/plugin.go
76+
```
77+
78+
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.
79+
80+
## Running Juno with the plugin
81+
82+
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.

docs/sidebars.js

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const sidebars = {
1212
"hardware-requirements",
1313
"running-juno",
1414
"configuring",
15+
"plugins",
1516
"running-on-gcp",
1617
"updating",
1718
],

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ require (
1414
github.com/ethereum/go-ethereum v1.14.11
1515
github.com/fxamacker/cbor/v2 v2.7.0
1616
github.com/go-playground/validator/v10 v10.22.1
17+
github.com/golang/protobuf v1.5.4
1718
github.com/jinzhu/copier v0.4.0
1819
github.com/libp2p/go-libp2p v0.36.2
1920
github.com/libp2p/go-libp2p-kad-dht v0.27.0

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
179179
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
180180
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
181181
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
182+
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
183+
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
182184
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
183185
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
184186
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=

0 commit comments

Comments
 (0)