Skip to content
23 changes: 23 additions & 0 deletions command/prune/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package prune

import (
"github.com/spf13/cobra"
)

func GetCommand() *cobra.Command {
pruneCmd := &cobra.Command{
Use: "prune-trie",
Short: "Prunes historical state trie data by copying only reachable nodes to a new database",
Long: `Prunes the state trie LevelDB by copying only nodes reachable from the latest
(or specified) block's state root to a new directory. The source trie is opened
read-only and never modified. After validation, the operator can swap directories.

Usage:
hydra prune-trie --data-dir ./node-secrets --target-path ./trie_new
hydra prune-trie --data-dir ./node-secrets --target-path ./trie_new --block 50000`,
}

pruneCmd.AddCommand(pruneTrieCmd())

return pruneCmd
}
39 changes: 39 additions & 0 deletions command/prune/get_state_root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package prune

import (
"fmt"

"github.com/0xPolygon/polygon-edge/blockchain/storage"
"github.com/0xPolygon/polygon-edge/types"
)

// GetLatestStateRoot reads the head block number from chain storage,
// then returns the state root from that block's header.
func GetLatestStateRoot(st storage.Storage) (types.Hash, uint64, error) {
headNum, ok := st.ReadHeadNumber()
if !ok {
return types.Hash{}, 0, fmt.Errorf("failed to read head number from chain storage")
}

root, err := GetStateRootAtBlock(st, headNum)
if err != nil {
return types.Hash{}, 0, fmt.Errorf("failed to get state root at head block %d: %w", headNum, err)
}

return root, headNum, nil
}

// GetStateRootAtBlock returns the state root from the header at a specific block number.
func GetStateRootAtBlock(st storage.Storage, blockNum uint64) (types.Hash, error) {
canonicalHash, ok := st.ReadCanonicalHash(blockNum)
if !ok {
return types.Hash{}, fmt.Errorf("failed to read canonical hash for block %d", blockNum)
}

header, err := st.ReadHeader(canonicalHash)
if err != nil {
return types.Hash{}, fmt.Errorf("failed to read header for block %d (hash %s): %w", blockNum, canonicalHash, err)
}

return header.StateRoot, nil
}
112 changes: 112 additions & 0 deletions command/prune/get_state_root_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package prune

import (
"errors"
"testing"

"github.com/0xPolygon/polygon-edge/blockchain/storage"
"github.com/0xPolygon/polygon-edge/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetLatestStateRoot_ReturnsHeaderStateRoot(t *testing.T) {
t.Parallel()

expectedRoot := types.StringToHash("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890")
blockHash := types.StringToHash("0x1111111111111111111111111111111111111111111111111111111111111111")

mock := storage.NewMockStorage()
mock.HookReadHeadNumber(func() (uint64, bool) {
return 100, true
})
mock.HookReadCanonicalHash(func(n uint64) (types.Hash, bool) {
if n == 100 {
return blockHash, true
}

return types.Hash{}, false
})
mock.HookReadHeader(func(hash types.Hash) (*types.Header, error) {
if hash == blockHash {
return &types.Header{
Number: 100,
StateRoot: expectedRoot,
}, nil
}

return nil, errors.New("not found")
})

root, blockNum, err := GetLatestStateRoot(mock)
require.NoError(t, err)
assert.Equal(t, expectedRoot, root)
assert.Equal(t, uint64(100), blockNum)
}

func TestGetLatestStateRoot_EmptyChain(t *testing.T) {
t.Parallel()

mock := storage.NewMockStorage()
mock.HookReadHeadNumber(func() (uint64, bool) {
return 0, false
})

_, _, err := GetLatestStateRoot(mock)
assert.Error(t, err)
assert.Contains(t, err.Error(), "head")
}

func TestGetStateRootAtBlock_ReturnsCorrectRoot(t *testing.T) {
t.Parallel()

roots := map[uint64]types.Hash{
5: types.StringToHash("0x5555555555555555555555555555555555555555555555555555555555555555"),
10: types.StringToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
}

hashes := map[uint64]types.Hash{
5: types.StringToHash("0x0505050505050505050505050505050505050505050505050505050505050505"),
10: types.StringToHash("0x0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a"),
}

mock := storage.NewMockStorage()
mock.HookReadCanonicalHash(func(n uint64) (types.Hash, bool) {
h, ok := hashes[n]

return h, ok
})
mock.HookReadHeader(func(hash types.Hash) (*types.Header, error) {
for num, h := range hashes {
if h == hash {
return &types.Header{
Number: num,
StateRoot: roots[num],
}, nil
}
}

return nil, errors.New("not found")
})

root, err := GetStateRootAtBlock(mock, 5)
require.NoError(t, err)
assert.Equal(t, roots[5], root)

root, err = GetStateRootAtBlock(mock, 10)
require.NoError(t, err)
assert.Equal(t, roots[10], root)
}

func TestGetStateRootAtBlock_NonexistentBlock(t *testing.T) {
t.Parallel()

mock := storage.NewMockStorage()
mock.HookReadCanonicalHash(func(n uint64) (types.Hash, bool) {
return types.Hash{}, false
})

_, err := GetStateRootAtBlock(mock, 999)
assert.Error(t, err)
assert.Contains(t, err.Error(), "canonical hash")
}
Loading
Loading