2020-07-01 18:44:59 +00:00
|
|
|
// Copyright © 2020 Vulcanize, Inc
|
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU Affero General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
package snapshot
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2020-07-01 23:07:56 +00:00
|
|
|
"errors"
|
2020-07-01 18:44:59 +00:00
|
|
|
"fmt"
|
2020-08-20 10:23:36 +00:00
|
|
|
"sync"
|
2020-07-01 18:44:59 +00:00
|
|
|
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
|
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
|
|
"github.com/ethereum/go-ethereum/core/state"
|
2022-01-08 03:32:45 +00:00
|
|
|
"github.com/ethereum/go-ethereum/core/types"
|
2020-07-01 18:44:59 +00:00
|
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
|
|
"github.com/ethereum/go-ethereum/ethdb"
|
|
|
|
"github.com/ethereum/go-ethereum/rlp"
|
|
|
|
"github.com/ethereum/go-ethereum/trie"
|
2022-03-09 13:37:33 +00:00
|
|
|
log "github.com/sirupsen/logrus"
|
2020-07-01 18:44:59 +00:00
|
|
|
|
2022-01-08 03:32:45 +00:00
|
|
|
iter "github.com/vulcanize/go-eth-state-node-iterator"
|
2022-03-30 23:57:30 +00:00
|
|
|
. "github.com/vulcanize/ipld-eth-state-snapshot/pkg/types"
|
2020-07-01 18:44:59 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
emptyNode, _ = rlp.EncodeToBytes([]byte{})
|
2020-07-31 05:31:34 +00:00
|
|
|
emptyCodeHash = crypto.Keccak256([]byte{})
|
2020-07-01 18:44:59 +00:00
|
|
|
emptyContractRoot = crypto.Keccak256Hash(emptyNode)
|
2021-12-13 15:01:32 +00:00
|
|
|
|
|
|
|
defaultBatchSize = uint(100)
|
2020-07-01 18:44:59 +00:00
|
|
|
)
|
|
|
|
|
2021-12-14 06:50:19 +00:00
|
|
|
// Service holds ethDB and stateDB to read data from lvldb and Publisher
|
|
|
|
// to publish trie in postgres DB.
|
2020-07-01 18:44:59 +00:00
|
|
|
type Service struct {
|
|
|
|
ethDB ethdb.Database
|
|
|
|
stateDB state.Database
|
2022-01-11 23:49:04 +00:00
|
|
|
ipfsPublisher Publisher
|
2021-12-13 15:01:32 +00:00
|
|
|
maxBatchSize uint
|
2022-02-18 11:12:53 +00:00
|
|
|
tracker iteratorTracker
|
|
|
|
recoveryFile string
|
2020-07-01 18:44:59 +00:00
|
|
|
}
|
|
|
|
|
2022-01-11 05:37:27 +00:00
|
|
|
func NewLevelDB(con *EthConfig) (ethdb.Database, error) {
|
2022-03-30 23:57:30 +00:00
|
|
|
edb, err := rawdb.NewLevelDBDatabaseWithFreezer(
|
|
|
|
con.LevelDBPath, 1024, 256, con.AncientDBPath, "ipld-eth-state-snapshot", true,
|
2022-01-11 05:37:27 +00:00
|
|
|
)
|
2022-03-30 23:57:30 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to create NewLevelDBDatabaseWithFreezer: %s", err)
|
|
|
|
}
|
|
|
|
return edb, nil
|
2022-01-11 05:37:27 +00:00
|
|
|
}
|
2021-12-13 15:01:32 +00:00
|
|
|
|
2022-01-11 05:37:27 +00:00
|
|
|
// NewSnapshotService creates Service.
|
2022-02-18 11:12:53 +00:00
|
|
|
func NewSnapshotService(edb ethdb.Database, pub Publisher, recoveryFile string) (*Service, error) {
|
2020-07-01 18:44:59 +00:00
|
|
|
return &Service{
|
|
|
|
ethDB: edb,
|
|
|
|
stateDB: state.NewDatabase(edb),
|
2022-01-11 00:59:26 +00:00
|
|
|
ipfsPublisher: pub,
|
2021-12-13 15:01:32 +00:00
|
|
|
maxBatchSize: defaultBatchSize,
|
2022-02-18 11:12:53 +00:00
|
|
|
recoveryFile: recoveryFile,
|
2020-07-01 18:44:59 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2020-08-20 10:23:36 +00:00
|
|
|
type SnapshotParams struct {
|
2022-01-08 03:32:45 +00:00
|
|
|
Height uint64
|
2020-08-31 16:03:37 +00:00
|
|
|
Workers uint
|
2020-08-20 10:23:36 +00:00
|
|
|
}
|
|
|
|
|
2020-08-23 04:38:31 +00:00
|
|
|
func (s *Service) CreateSnapshot(params SnapshotParams) error {
|
2020-07-16 15:31:37 +00:00
|
|
|
// extract header from lvldb and publish to PG-IPFS
|
|
|
|
// hold onto the headerID so that we can link the state nodes to this header
|
2022-03-09 13:37:33 +00:00
|
|
|
log.Infof("Creating snapshot at height %d", params.Height)
|
2020-08-23 04:38:31 +00:00
|
|
|
hash := rawdb.ReadCanonicalHash(s.ethDB, params.Height)
|
|
|
|
header := rawdb.ReadHeader(s.ethDB, hash, params.Height)
|
2020-07-16 15:31:37 +00:00
|
|
|
if header == nil {
|
2020-08-23 04:38:31 +00:00
|
|
|
return fmt.Errorf("unable to read canonical header at height %d", params.Height)
|
2020-07-16 15:31:37 +00:00
|
|
|
}
|
2021-12-13 15:01:32 +00:00
|
|
|
|
2022-03-09 13:37:33 +00:00
|
|
|
log.Infof("head hash: %s head height: %d", hash.Hex(), params.Height)
|
2021-12-13 15:01:32 +00:00
|
|
|
|
2022-02-09 15:19:10 +00:00
|
|
|
err := s.ipfsPublisher.PublishHeader(header)
|
2020-07-16 15:31:37 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-08-23 04:38:31 +00:00
|
|
|
|
2022-02-18 11:12:53 +00:00
|
|
|
tree, err := s.stateDB.OpenTrie(header.Root)
|
2020-07-16 15:31:37 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-02-09 15:19:10 +00:00
|
|
|
headerID := header.Hash().String()
|
2022-03-11 14:28:32 +00:00
|
|
|
s.tracker = newTracker(s.recoveryFile, int(params.Workers))
|
2022-02-18 11:12:53 +00:00
|
|
|
go s.tracker.run()
|
2022-03-11 14:28:32 +00:00
|
|
|
go s.tracker.captureSignal()
|
2022-02-09 15:19:10 +00:00
|
|
|
|
2022-02-18 11:12:53 +00:00
|
|
|
var iters []trie.NodeIterator
|
|
|
|
// attempt to restore from recovery file if it exists
|
2022-03-11 14:28:32 +00:00
|
|
|
iters, err = s.tracker.restore(tree)
|
2022-02-18 11:12:53 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if iters != nil {
|
|
|
|
if params.Workers < uint(len(iters)) {
|
|
|
|
return fmt.Errorf(
|
|
|
|
"number of recovered workers (%d) is greater than number configured (%d)",
|
|
|
|
len(iters), params.Workers,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} else { // nothing to restore
|
|
|
|
if params.Workers > 1 {
|
|
|
|
iters = iter.SubtrieIterators(tree, params.Workers)
|
|
|
|
} else {
|
|
|
|
iters = []trie.NodeIterator{tree.NodeIterator(nil)}
|
|
|
|
}
|
|
|
|
for i, it := range iters {
|
|
|
|
iters[i] = s.tracker.tracked(it)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func() {
|
2022-03-11 14:28:32 +00:00
|
|
|
err := s.tracker.haltAndDump()
|
2022-02-18 11:12:53 +00:00
|
|
|
if err != nil {
|
2022-03-09 13:37:33 +00:00
|
|
|
log.Error("failed to write recovery file: ", err)
|
2022-02-18 11:12:53 +00:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
if len(iters) > 0 {
|
|
|
|
return s.createSnapshotAsync(iters, headerID)
|
2020-08-20 10:23:36 +00:00
|
|
|
} else {
|
2022-02-18 11:12:53 +00:00
|
|
|
return s.createSnapshot(iters[0], headerID)
|
2020-08-20 10:23:36 +00:00
|
|
|
}
|
|
|
|
return nil
|
2020-07-16 15:31:37 +00:00
|
|
|
}
|
|
|
|
|
2020-08-20 10:23:36 +00:00
|
|
|
// Create snapshot up to head (ignores height param)
|
2022-01-10 23:47:27 +00:00
|
|
|
func (s *Service) CreateLatestSnapshot(workers uint) error {
|
2022-03-09 13:37:33 +00:00
|
|
|
log.Info("Creating snapshot at head")
|
2020-08-20 10:23:36 +00:00
|
|
|
hash := rawdb.ReadHeadHeaderHash(s.ethDB)
|
|
|
|
height := rawdb.ReadHeaderNumber(s.ethDB, hash)
|
|
|
|
if height == nil {
|
|
|
|
return fmt.Errorf("unable to read header height for header hash %s", hash.String())
|
2020-07-16 15:02:16 +00:00
|
|
|
}
|
2022-01-10 23:47:27 +00:00
|
|
|
return s.CreateSnapshot(SnapshotParams{Height: *height, Workers: workers})
|
2020-08-20 10:23:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type nodeResult struct {
|
2022-01-11 23:49:04 +00:00
|
|
|
node Node
|
2020-08-20 10:23:36 +00:00
|
|
|
elements []interface{}
|
|
|
|
}
|
|
|
|
|
2020-09-06 07:36:36 +00:00
|
|
|
func resolveNode(it trie.NodeIterator, trieDB *trie.Database) (*nodeResult, error) {
|
2022-01-11 23:49:04 +00:00
|
|
|
// "leaf" nodes are actually "value" nodes, whose parents are the actual leaves
|
|
|
|
if it.Leaf() {
|
|
|
|
return nil, nil
|
|
|
|
}
|
2022-02-09 15:19:10 +00:00
|
|
|
if IsNullHash(it.Hash()) {
|
2022-01-11 23:49:04 +00:00
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2022-01-11 00:06:29 +00:00
|
|
|
path := make([]byte, len(it.Path()))
|
|
|
|
copy(path, it.Path())
|
|
|
|
n, err := trieDB.Node(it.Hash())
|
2020-07-01 18:44:59 +00:00
|
|
|
if err != nil {
|
2020-08-20 10:23:36 +00:00
|
|
|
return nil, err
|
2020-07-01 18:44:59 +00:00
|
|
|
}
|
2022-01-11 00:06:29 +00:00
|
|
|
var elements []interface{}
|
|
|
|
if err := rlp.DecodeBytes(n, &elements); err != nil {
|
2020-08-20 10:23:36 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2022-01-11 00:06:29 +00:00
|
|
|
ty, err := CheckKeyType(elements)
|
2020-07-01 18:44:59 +00:00
|
|
|
if err != nil {
|
2020-08-20 10:23:36 +00:00
|
|
|
return nil, err
|
2020-07-01 18:44:59 +00:00
|
|
|
}
|
2020-08-20 10:23:36 +00:00
|
|
|
return &nodeResult{
|
2022-01-11 23:49:04 +00:00
|
|
|
node: Node{
|
|
|
|
NodeType: ty,
|
|
|
|
Path: path,
|
|
|
|
Value: n,
|
2020-08-20 10:23:36 +00:00
|
|
|
},
|
2022-01-11 00:06:29 +00:00
|
|
|
elements: elements,
|
2020-08-20 10:23:36 +00:00
|
|
|
}, nil
|
2020-07-01 18:44:59 +00:00
|
|
|
}
|
|
|
|
|
2022-02-09 15:19:10 +00:00
|
|
|
func (s *Service) createSnapshot(it trie.NodeIterator, headerID string) error {
|
2022-01-11 23:49:04 +00:00
|
|
|
tx, err := s.ipfsPublisher.BeginTx()
|
2022-01-11 00:06:29 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-02-09 15:19:10 +00:00
|
|
|
defer func() { err = CommitOrRollback(tx, err) }()
|
2022-01-11 00:06:29 +00:00
|
|
|
|
2022-01-08 03:32:45 +00:00
|
|
|
for it.Next(true) {
|
2022-01-11 23:49:04 +00:00
|
|
|
res, err := resolveNode(it, s.stateDB.TrieDB())
|
2021-12-13 15:01:32 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-01-11 23:49:04 +00:00
|
|
|
if res == nil {
|
|
|
|
continue
|
|
|
|
}
|
2021-12-13 15:01:32 +00:00
|
|
|
|
2022-01-11 23:49:04 +00:00
|
|
|
tx, err = s.ipfsPublisher.PrepareTxForBatch(tx, s.maxBatchSize)
|
2020-07-01 18:44:59 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-01-11 23:49:04 +00:00
|
|
|
|
|
|
|
switch res.node.NodeType {
|
|
|
|
case Leaf:
|
2022-02-18 11:12:53 +00:00
|
|
|
// if the node is a leaf, decode the account and publish the associated storage trie
|
|
|
|
// nodes if there are any
|
2022-01-08 03:32:45 +00:00
|
|
|
var account types.StateAccount
|
|
|
|
if err := rlp.DecodeBytes(res.elements[1].([]byte), &account); err != nil {
|
|
|
|
return fmt.Errorf(
|
2022-01-11 23:49:04 +00:00
|
|
|
"error decoding account for leaf node at path %x nerror: %v", res.node.Path, err)
|
2022-01-08 03:32:45 +00:00
|
|
|
}
|
|
|
|
partialPath := trie.CompactToHex(res.elements[0].([]byte))
|
2022-01-11 23:49:04 +00:00
|
|
|
valueNodePath := append(res.node.Path, partialPath...)
|
2022-01-08 03:32:45 +00:00
|
|
|
encodedPath := trie.HexToCompact(valueNodePath)
|
|
|
|
leafKey := encodedPath[1:]
|
2022-01-11 23:49:04 +00:00
|
|
|
res.node.Key = common.BytesToHash(leafKey)
|
2022-02-09 15:19:10 +00:00
|
|
|
err := s.ipfsPublisher.PublishStateNode(&res.node, headerID, tx)
|
2020-07-01 23:07:56 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-12-13 15:01:32 +00:00
|
|
|
|
2022-01-08 03:32:45 +00:00
|
|
|
// publish any non-nil code referenced by codehash
|
|
|
|
if !bytes.Equal(account.CodeHash, emptyCodeHash) {
|
2021-12-15 07:23:18 +00:00
|
|
|
codeHash := common.BytesToHash(account.CodeHash)
|
|
|
|
codeBytes := rawdb.ReadCode(s.ethDB, codeHash)
|
2021-12-13 15:01:32 +00:00
|
|
|
if len(codeBytes) == 0 {
|
2022-03-09 13:37:33 +00:00
|
|
|
log.Error("Code is missing", "account", common.BytesToHash(it.LeafKey()))
|
2021-12-13 15:01:32 +00:00
|
|
|
return errors.New("missing code")
|
2022-01-08 03:32:45 +00:00
|
|
|
}
|
2021-12-13 15:01:32 +00:00
|
|
|
|
2021-12-15 07:23:18 +00:00
|
|
|
if err = s.ipfsPublisher.PublishCode(codeHash, codeBytes, tx); err != nil {
|
2022-01-08 03:32:45 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2021-12-13 15:01:32 +00:00
|
|
|
|
2022-02-09 15:19:10 +00:00
|
|
|
if tx, err = s.storageSnapshot(account.Root, headerID, res.node.Path, tx); err != nil {
|
2021-12-14 06:50:19 +00:00
|
|
|
return fmt.Errorf("failed building storage snapshot for account %+v\r\nerror: %w", account, err)
|
2022-01-08 03:32:45 +00:00
|
|
|
}
|
2022-01-11 23:49:04 +00:00
|
|
|
case Extension, Branch:
|
|
|
|
res.node.Key = common.BytesToHash([]byte{})
|
2022-02-09 15:19:10 +00:00
|
|
|
if err := s.ipfsPublisher.PublishStateNode(&res.node, headerID, tx); err != nil {
|
2020-07-01 23:07:56 +00:00
|
|
|
return err
|
|
|
|
}
|
2022-01-08 03:32:45 +00:00
|
|
|
default:
|
|
|
|
return errors.New("unexpected node type")
|
2020-08-20 10:23:36 +00:00
|
|
|
}
|
2020-07-01 18:44:59 +00:00
|
|
|
}
|
2020-08-03 15:46:35 +00:00
|
|
|
return it.Error()
|
2020-07-01 18:44:59 +00:00
|
|
|
}
|
|
|
|
|
2022-01-08 03:32:45 +00:00
|
|
|
// Full-trie concurrent snapshot
|
2022-02-18 11:12:53 +00:00
|
|
|
func (s *Service) createSnapshotAsync(iters []trie.NodeIterator, headerID string) error {
|
2020-08-23 04:38:31 +00:00
|
|
|
errors := make(chan error)
|
|
|
|
var wg sync.WaitGroup
|
2022-02-18 11:12:53 +00:00
|
|
|
for _, it := range iters {
|
2020-08-23 04:38:31 +00:00
|
|
|
wg.Add(1)
|
2022-02-18 11:12:53 +00:00
|
|
|
go func(it trie.NodeIterator) {
|
2020-08-23 04:38:31 +00:00
|
|
|
defer wg.Done()
|
|
|
|
if err := s.createSnapshot(it, headerID); err != nil {
|
|
|
|
errors <- err
|
|
|
|
}
|
2022-02-18 11:12:53 +00:00
|
|
|
}(it)
|
2020-09-06 07:36:36 +00:00
|
|
|
}
|
2022-02-18 11:12:53 +00:00
|
|
|
|
|
|
|
done := make(chan struct{})
|
2020-08-23 04:38:31 +00:00
|
|
|
go func() {
|
|
|
|
wg.Wait()
|
2022-02-18 11:12:53 +00:00
|
|
|
done <- struct{}{}
|
2020-08-23 04:38:31 +00:00
|
|
|
}()
|
2020-08-20 10:23:36 +00:00
|
|
|
|
2022-02-18 11:12:53 +00:00
|
|
|
var err error
|
2020-08-23 04:38:31 +00:00
|
|
|
select {
|
2022-02-18 11:12:53 +00:00
|
|
|
case err = <-errors:
|
|
|
|
case <-done:
|
|
|
|
close(errors)
|
2020-08-20 10:23:36 +00:00
|
|
|
}
|
2022-02-18 11:12:53 +00:00
|
|
|
return err
|
2020-08-20 10:23:36 +00:00
|
|
|
}
|
|
|
|
|
2022-02-09 15:19:10 +00:00
|
|
|
func (s *Service) storageSnapshot(sr common.Hash, headerID string, statePath []byte, tx Tx) (Tx, error) {
|
2020-07-01 18:44:59 +00:00
|
|
|
if bytes.Equal(sr.Bytes(), emptyContractRoot.Bytes()) {
|
2021-12-13 15:01:32 +00:00
|
|
|
return tx, nil
|
2020-07-01 18:44:59 +00:00
|
|
|
}
|
2021-12-13 15:01:32 +00:00
|
|
|
|
2020-07-01 18:44:59 +00:00
|
|
|
sTrie, err := s.stateDB.OpenTrie(sr)
|
|
|
|
if err != nil {
|
2021-12-13 15:01:32 +00:00
|
|
|
return nil, err
|
2020-07-01 18:44:59 +00:00
|
|
|
}
|
2021-12-13 15:01:32 +00:00
|
|
|
|
2020-07-01 18:44:59 +00:00
|
|
|
it := sTrie.NodeIterator(make([]byte, 0))
|
|
|
|
for it.Next(true) {
|
2020-08-20 10:23:36 +00:00
|
|
|
res, err := resolveNode(it, s.stateDB.TrieDB())
|
2020-07-01 18:44:59 +00:00
|
|
|
if err != nil {
|
2021-12-13 15:01:32 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2022-01-11 23:49:04 +00:00
|
|
|
if res == nil {
|
|
|
|
continue
|
|
|
|
}
|
2021-12-13 15:01:32 +00:00
|
|
|
|
2022-01-11 23:49:04 +00:00
|
|
|
tx, err = s.ipfsPublisher.PrepareTxForBatch(tx, s.maxBatchSize)
|
2020-07-01 18:44:59 +00:00
|
|
|
if err != nil {
|
2021-12-13 15:01:32 +00:00
|
|
|
return nil, err
|
2020-07-01 18:44:59 +00:00
|
|
|
}
|
2021-12-13 15:01:32 +00:00
|
|
|
|
2022-01-11 00:06:29 +00:00
|
|
|
var nodeData []byte
|
2022-01-10 23:47:27 +00:00
|
|
|
nodeData, err = s.stateDB.TrieDB().Node(it.Hash())
|
2020-07-01 18:44:59 +00:00
|
|
|
if err != nil {
|
2021-12-13 15:01:32 +00:00
|
|
|
return nil, err
|
2020-07-01 18:44:59 +00:00
|
|
|
}
|
2022-01-11 23:49:04 +00:00
|
|
|
res.node.Value = nodeData
|
2022-01-11 00:06:29 +00:00
|
|
|
|
2022-01-11 23:49:04 +00:00
|
|
|
switch res.node.NodeType {
|
|
|
|
case Leaf:
|
2020-08-20 10:23:36 +00:00
|
|
|
partialPath := trie.CompactToHex(res.elements[0].([]byte))
|
2022-01-11 23:49:04 +00:00
|
|
|
valueNodePath := append(res.node.Path, partialPath...)
|
2020-07-01 18:44:59 +00:00
|
|
|
encodedPath := trie.HexToCompact(valueNodePath)
|
|
|
|
leafKey := encodedPath[1:]
|
2022-01-11 23:49:04 +00:00
|
|
|
res.node.Key = common.BytesToHash(leafKey)
|
|
|
|
case Extension, Branch:
|
|
|
|
res.node.Key = common.BytesToHash([]byte{})
|
2020-07-01 18:44:59 +00:00
|
|
|
default:
|
2021-12-13 15:01:32 +00:00
|
|
|
return nil, errors.New("unexpected node type")
|
2020-07-01 23:07:56 +00:00
|
|
|
}
|
2022-02-09 15:19:10 +00:00
|
|
|
if err = s.ipfsPublisher.PublishStorageNode(&res.node, headerID, statePath, tx); err != nil {
|
2022-01-11 00:06:29 +00:00
|
|
|
return nil, err
|
2020-07-01 18:44:59 +00:00
|
|
|
}
|
|
|
|
}
|
2021-12-13 15:01:32 +00:00
|
|
|
|
|
|
|
return tx, it.Error()
|
2020-07-01 18:44:59 +00:00
|
|
|
}
|