retrieval: Client impl
This commit is contained in:
parent
28d3eb38eb
commit
60eedb699e
@ -92,7 +92,7 @@ type FullNode interface {
|
|||||||
ClientImport(ctx context.Context, path string) (cid.Cid, error)
|
ClientImport(ctx context.Context, path string) (cid.Cid, error)
|
||||||
ClientStartDeal(ctx context.Context, data cid.Cid, miner address.Address, price types.BigInt, blocksDuration uint64) (*cid.Cid, error)
|
ClientStartDeal(ctx context.Context, data cid.Cid, miner address.Address, price types.BigInt, blocksDuration uint64) (*cid.Cid, error)
|
||||||
ClientHasLocal(ctx context.Context, root cid.Cid) (bool, error)
|
ClientHasLocal(ctx context.Context, root cid.Cid) (bool, error)
|
||||||
ClientFindData(ctx context.Context, root cid.Cid) ([]RetrievalOffer, error) // TODO: specify serialization mode we want (defaults to unixfs for now)
|
ClientFindData(ctx context.Context, root cid.Cid) ([]QueryOffer, error) // TODO: specify serialization mode we want (defaults to unixfs for now)
|
||||||
|
|
||||||
// ClientUnimport removes references to the specified file from filestore
|
// ClientUnimport removes references to the specified file from filestore
|
||||||
//ClientUnimport(path string)
|
//ClientUnimport(path string)
|
||||||
@ -193,7 +193,7 @@ type SealedRef struct {
|
|||||||
Size uint32
|
Size uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
type RetrievalOffer struct {
|
type QueryOffer struct {
|
||||||
Err string
|
Err string
|
||||||
|
|
||||||
Size uint64
|
Size uint64
|
||||||
|
@ -71,7 +71,7 @@ type FullNodeStruct struct {
|
|||||||
ClientImport func(ctx context.Context, path string) (cid.Cid, error) `perm:"write"`
|
ClientImport func(ctx context.Context, path string) (cid.Cid, error) `perm:"write"`
|
||||||
ClientListImports func(ctx context.Context) ([]Import, error) `perm:"write"`
|
ClientListImports func(ctx context.Context) ([]Import, error) `perm:"write"`
|
||||||
ClientHasLocal func(ctx context.Context, root cid.Cid) (bool, error) `perm:"write"`
|
ClientHasLocal func(ctx context.Context, root cid.Cid) (bool, error) `perm:"write"`
|
||||||
ClientFindData func(ctx context.Context, root cid.Cid) ([]RetrievalOffer, error) `perm:"read"`
|
ClientFindData func(ctx context.Context, root cid.Cid) ([]QueryOffer, error) `perm:"read"`
|
||||||
ClientStartDeal func(ctx context.Context, data cid.Cid, miner address.Address, price types.BigInt, blocksDuration uint64) (*cid.Cid, error) `perm:"admin"`
|
ClientStartDeal func(ctx context.Context, data cid.Cid, miner address.Address, price types.BigInt, blocksDuration uint64) (*cid.Cid, error) `perm:"admin"`
|
||||||
|
|
||||||
StateMinerSectors func(context.Context, address.Address) ([]*SectorInfo, error) `perm:"read"`
|
StateMinerSectors func(context.Context, address.Address) ([]*SectorInfo, error) `perm:"read"`
|
||||||
@ -158,7 +158,7 @@ func (c *FullNodeStruct) ClientHasLocal(ctx context.Context, root cid.Cid) (bool
|
|||||||
return c.Internal.ClientHasLocal(ctx, root)
|
return c.Internal.ClientHasLocal(ctx, root)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *FullNodeStruct) ClientFindData(ctx context.Context, root cid.Cid) ([]RetrievalOffer, error) {
|
func (c *FullNodeStruct) ClientFindData(ctx context.Context, root cid.Cid) ([]QueryOffer, error) {
|
||||||
return c.Internal.ClientFindData(ctx, root)
|
return c.Internal.ClientFindData(ctx, root)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
7
build/params.go
Normal file
7
build/params.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package build
|
||||||
|
|
||||||
|
// Core network constants
|
||||||
|
|
||||||
|
const UnixfsChunkSize uint64 = 1 << 20
|
||||||
|
|
||||||
|
// TODO: Move other important consts here
|
1
go.mod
1
go.mod
@ -53,6 +53,7 @@ require (
|
|||||||
github.com/libp2p/go-libp2p-tls v0.1.0
|
github.com/libp2p/go-libp2p-tls v0.1.0
|
||||||
github.com/libp2p/go-libp2p-yamux v0.2.1
|
github.com/libp2p/go-libp2p-yamux v0.2.1
|
||||||
github.com/libp2p/go-maddr-filter v0.0.5
|
github.com/libp2p/go-maddr-filter v0.0.5
|
||||||
|
github.com/libp2p/go-msgio v0.0.4
|
||||||
github.com/miekg/dns v1.1.16 // indirect
|
github.com/miekg/dns v1.1.16 // indirect
|
||||||
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1
|
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
|
@ -3,6 +3,7 @@ package full
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/filecoin-project/go-lotus/build"
|
||||||
"github.com/filecoin-project/go-lotus/retrieval"
|
"github.com/filecoin-project/go-lotus/retrieval"
|
||||||
"github.com/filecoin-project/go-lotus/retrieval/discovery"
|
"github.com/filecoin-project/go-lotus/retrieval/discovery"
|
||||||
"github.com/ipfs/go-blockservice"
|
"github.com/ipfs/go-blockservice"
|
||||||
@ -138,13 +139,13 @@ func (a *ClientAPI) ClientHasLocal(ctx context.Context, root cid.Cid) (bool, err
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ClientAPI) ClientFindData(ctx context.Context, root cid.Cid) ([]api.RetrievalOffer, error) {
|
func (a *ClientAPI) ClientFindData(ctx context.Context, root cid.Cid) ([]api.QueryOffer, error) {
|
||||||
peers, err := a.RetDiscovery.GetPeers(root)
|
peers, err := a.RetDiscovery.GetPeers(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
out := make([]api.RetrievalOffer, len(peers))
|
out := make([]api.QueryOffer, len(peers))
|
||||||
for k, p := range peers {
|
for k, p := range peers {
|
||||||
out[k] = a.Retrieval.Query(ctx, p, root)
|
out[k] = a.Retrieval.Query(ctx, p, root)
|
||||||
}
|
}
|
||||||
@ -177,7 +178,7 @@ func (a *ClientAPI) ClientImport(ctx context.Context, path string) (cid.Cid, err
|
|||||||
NoCopy: true,
|
NoCopy: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := params.New(chunker.DefaultSplitter(file))
|
db, err := params.New(chunker.NewSizeSplitter(file, build.UnixfsChunkSize))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cid.Undef, err
|
return cid.Undef, err
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,23 @@ package retrieval
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/filecoin-project/go-lotus/lib/cborrpc"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
|
||||||
|
blocks "github.com/ipfs/go-block-format"
|
||||||
|
"github.com/libp2p/go-libp2p-core/network"
|
||||||
|
"github.com/libp2p/go-libp2p-core/peer"
|
||||||
|
"github.com/libp2p/go-msgio"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
pb "github.com/ipfs/go-bitswap/message/pb"
|
||||||
"github.com/ipfs/go-cid"
|
"github.com/ipfs/go-cid"
|
||||||
cbor "github.com/ipfs/go-ipld-cbor"
|
cbor "github.com/ipfs/go-ipld-cbor"
|
||||||
logging "github.com/ipfs/go-log"
|
logging "github.com/ipfs/go-log"
|
||||||
"github.com/libp2p/go-libp2p-core/host"
|
"github.com/libp2p/go-libp2p-core/host"
|
||||||
|
|
||||||
"github.com/filecoin-project/go-lotus/api"
|
"github.com/filecoin-project/go-lotus/api"
|
||||||
|
"github.com/filecoin-project/go-lotus/build"
|
||||||
|
"github.com/filecoin-project/go-lotus/chain/address"
|
||||||
|
"github.com/filecoin-project/go-lotus/lib/cborrpc"
|
||||||
"github.com/filecoin-project/go-lotus/retrieval/discovery"
|
"github.com/filecoin-project/go-lotus/retrieval/discovery"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -24,39 +32,180 @@ func NewClient(h host.Host) *Client {
|
|||||||
return &Client{h: h}
|
return &Client{h: h}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Query(ctx context.Context, p discovery.RetrievalPeer, data cid.Cid) api.RetrievalOffer {
|
func (c *Client) Query(ctx context.Context, p discovery.RetrievalPeer, data cid.Cid) api.QueryOffer {
|
||||||
s, err := c.h.NewStream(ctx, p.ID, QueryProtocolID)
|
s, err := c.h.NewStream(ctx, p.ID, QueryProtocolID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn(err)
|
log.Warn(err)
|
||||||
return api.RetrievalOffer{Err: err.Error(), Miner: p.Address, MinerPeerID: p.ID}
|
return api.QueryOffer{Err: err.Error(), Miner: p.Address, MinerPeerID: p.ID}
|
||||||
}
|
}
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
err = cborrpc.WriteCborRPC(s, RetQuery{
|
err = cborrpc.WriteCborRPC(s, Query{
|
||||||
Piece: data,
|
Piece: data,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn(err)
|
log.Warn(err)
|
||||||
return api.RetrievalOffer{Err: err.Error(), Miner: p.Address, MinerPeerID: p.ID}
|
return api.QueryOffer{Err: err.Error(), Miner: p.Address, MinerPeerID: p.ID}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: read deadline
|
// TODO: read deadline
|
||||||
rawResp, err := ioutil.ReadAll(s)
|
rawResp, err := ioutil.ReadAll(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn(err)
|
log.Warn(err)
|
||||||
return api.RetrievalOffer{Err: err.Error(), Miner: p.Address, MinerPeerID: p.ID}
|
return api.QueryOffer{Err: err.Error(), Miner: p.Address, MinerPeerID: p.ID}
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp RetQueryResponse
|
var resp QueryResponse
|
||||||
if err := cbor.DecodeInto(rawResp, &resp); err != nil {
|
if err := cbor.DecodeInto(rawResp, &resp); err != nil {
|
||||||
log.Warn(err)
|
log.Warn(err)
|
||||||
return api.RetrievalOffer{Err: err.Error(), Miner: p.Address, MinerPeerID: p.ID}
|
return api.QueryOffer{Err: err.Error(), Miner: p.Address, MinerPeerID: p.ID}
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.RetrievalOffer{
|
return api.QueryOffer{
|
||||||
Size: resp.Size,
|
Size: resp.Size,
|
||||||
MinPrice: resp.MinPrice,
|
MinPrice: resp.MinPrice,
|
||||||
Miner: p.Address, // TODO: check
|
Miner: p.Address, // TODO: check
|
||||||
MinerPeerID: p.ID,
|
MinerPeerID: p.ID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type clientStream struct {
|
||||||
|
stream network.Stream
|
||||||
|
|
||||||
|
root cid.Cid
|
||||||
|
offset uint64
|
||||||
|
|
||||||
|
windowSize uint64 // how much we "trust" the peer
|
||||||
|
verifier BlockVerifier
|
||||||
|
}
|
||||||
|
|
||||||
|
// C > S
|
||||||
|
//
|
||||||
|
// Offset MUST be aligned on chunking boundaries, size is rounded up to leaf size
|
||||||
|
//
|
||||||
|
// > Deal{Mode: Unixfs0, RootCid, Offset, Size, Payment(nil if free)}
|
||||||
|
// < Resp{Accept}
|
||||||
|
// < ..(Intermediate Block)
|
||||||
|
// < ..Blocks
|
||||||
|
// < ..(Intermediate Block)
|
||||||
|
// < ..Blocks
|
||||||
|
// > Deal(...)
|
||||||
|
// < ...
|
||||||
|
func (c *Client) RetrieveUnixfs(ctx context.Context, root cid.Cid, size uint64, miner peer.ID, minerAddr address.Address) error {
|
||||||
|
s, err := c.h.NewStream(ctx, miner, ProtocolID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
cst := clientStream{
|
||||||
|
stream: s,
|
||||||
|
|
||||||
|
root: root,
|
||||||
|
offset: 0, // TODO: check how much data we have locally
|
||||||
|
|
||||||
|
windowSize: build.UnixfsChunkSize,
|
||||||
|
verifier: &OptimisticVerifier{}, // TODO: Use a real verifier
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
toFetch := cst.windowSize
|
||||||
|
if toFetch+cst.offset > size {
|
||||||
|
toFetch = size - cst.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cst.doOneExchange(toFetch)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cst *clientStream) doOneExchange(toFetch uint64) error {
|
||||||
|
deal := Deal{Unixfs0: &Unixfs0Offer{
|
||||||
|
Root: cst.root,
|
||||||
|
Offset: cst.offset,
|
||||||
|
Size: toFetch,
|
||||||
|
}}
|
||||||
|
|
||||||
|
if err := cborrpc.WriteCborRPC(cst.stream, deal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp DealResponse
|
||||||
|
if err := cborrpc.ReadCborRPC(cst.stream, &resp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.AcceptedResponse == nil {
|
||||||
|
cst.windowSize = build.UnixfsChunkSize
|
||||||
|
// TODO: apply some 'penalty' to miner 'reputation' (needs to be the same in both cases)
|
||||||
|
|
||||||
|
if resp.ErrorResponse != nil {
|
||||||
|
return xerrors.Errorf("storage deal error: %s", resp.ErrorResponse.Message)
|
||||||
|
}
|
||||||
|
if resp.RejectedResponse != nil {
|
||||||
|
return xerrors.Errorf("storage deal rejected: %s", resp.RejectedResponse.Message)
|
||||||
|
}
|
||||||
|
return xerrors.New("storage deal response had no Accepted section")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cst.fetchBlocks(toFetch)
|
||||||
|
|
||||||
|
// TODO: maybe increase miner window size after success
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cst *clientStream) fetchBlocks(toFetch uint64) error {
|
||||||
|
blocksToFetch := (toFetch + build.UnixfsChunkSize - 1) / build.UnixfsChunkSize
|
||||||
|
|
||||||
|
// TODO: put msgio into spec
|
||||||
|
reader := msgio.NewVarintReaderSize(cst.stream, network.MessageSizeMax)
|
||||||
|
|
||||||
|
for i := uint64(0); i < blocksToFetch; {
|
||||||
|
msg, err := reader.ReadMsg()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var pb pb.Message_Block
|
||||||
|
if err := pb.Unmarshal(msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataBlocks, err := cst.consumeBlockMessage(pb)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
i += dataBlocks
|
||||||
|
|
||||||
|
reader.ReleaseMsg(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cst *clientStream) consumeBlockMessage(pb pb.Message_Block) (uint64, error) {
|
||||||
|
prefix, err := cid.PrefixFromBytes(pb.GetPrefix())
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
cid, err := prefix.Sum(pb.GetData())
|
||||||
|
|
||||||
|
blk, err := blocks.NewBlockWithCid(pb.GetData(), cid)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
internal, err := cst.verifier.Verify(blk)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Persist block
|
||||||
|
|
||||||
|
if internal {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
@ -21,7 +21,7 @@ func NewMiner(sblks *sectorblocks.SectorBlocks) *Miner {
|
|||||||
func (m *Miner) HandleStream(stream network.Stream) {
|
func (m *Miner) HandleStream(stream network.Stream) {
|
||||||
defer stream.Close()
|
defer stream.Close()
|
||||||
|
|
||||||
var query RetQuery
|
var query Query
|
||||||
if err := cborrpc.ReadCborRPC(stream, &query); err != nil {
|
if err := cborrpc.ReadCborRPC(stream, &query); err != nil {
|
||||||
log.Errorf("Retrieval query: ReadCborRPC: %s", err)
|
log.Errorf("Retrieval query: ReadCborRPC: %s", err)
|
||||||
return
|
return
|
||||||
@ -33,15 +33,15 @@ func (m *Miner) HandleStream(stream network.Stream) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
answer := RetQueryResponse{
|
answer := QueryResponse{
|
||||||
Status: Unavailable,
|
Status: Unavailable,
|
||||||
}
|
}
|
||||||
if len(refs) > 0 {
|
if len(refs) > 0 {
|
||||||
answer.Status = Available
|
answer.Status = Available
|
||||||
|
|
||||||
// TODO: get price, look for already unsealed ref to reduce work
|
// TODO: get price, look for already unsealed ref to reduce work
|
||||||
answer.MinPrice = types.NewInt(uint64(refs[0].Size)) // TODO: Get this from somewhere
|
answer.MinPrice = types.NewInt(uint64(refs[0].Size) * 2) // TODO: Get this from somewhere
|
||||||
answer.Size = uint64(refs[0].Size)
|
answer.Size = uint64(refs[0].Size) // TODO: verify on intermediate
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cborrpc.WriteCborRPC(stream, answer); err != nil {
|
if err := cborrpc.WriteCborRPC(stream, answer); err != nil {
|
||||||
|
@ -10,35 +10,53 @@ import (
|
|||||||
const ProtocolID = "/fil/retrieval/-1.0.0" // TODO: spec
|
const ProtocolID = "/fil/retrieval/-1.0.0" // TODO: spec
|
||||||
const QueryProtocolID = "/fil/retrieval/qry/-1.0.0" // TODO: spec
|
const QueryProtocolID = "/fil/retrieval/qry/-1.0.0" // TODO: spec
|
||||||
|
|
||||||
type QueryResponse int
|
type QueryResponseStatus int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Available QueryResponse = iota
|
Available QueryResponseStatus = iota
|
||||||
Unavailable
|
Unavailable
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cbor.RegisterCborType(RetDealProposal{})
|
cbor.RegisterCborType(Deal{})
|
||||||
|
|
||||||
cbor.RegisterCborType(RetQuery{})
|
cbor.RegisterCborType(Query{})
|
||||||
cbor.RegisterCborType(RetQueryResponse{})
|
cbor.RegisterCborType(QueryResponse{})
|
||||||
}
|
}
|
||||||
|
|
||||||
type RetDealProposal struct {
|
type Query struct {
|
||||||
Piece cid.Cid
|
|
||||||
Price types.BigInt
|
|
||||||
Payment types.SignedVoucher
|
|
||||||
}
|
|
||||||
|
|
||||||
type RetQuery struct {
|
|
||||||
Piece cid.Cid
|
Piece cid.Cid
|
||||||
|
// TODO: payment
|
||||||
}
|
}
|
||||||
|
|
||||||
type RetQueryResponse struct {
|
type QueryResponse struct {
|
||||||
Status QueryResponse
|
Status QueryResponseStatus
|
||||||
|
|
||||||
Size uint64 // TODO: spec
|
Size uint64 // TODO: spec
|
||||||
// TODO: unseal price (+spec)
|
// TODO: unseal price (+spec)
|
||||||
|
// TODO: sectors to unseal
|
||||||
// TODO: address to send money for the deal?
|
// TODO: address to send money for the deal?
|
||||||
MinPrice types.BigInt
|
MinPrice types.BigInt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Unixfs0Offer struct {
|
||||||
|
Root cid.Cid
|
||||||
|
Offset uint64
|
||||||
|
Size uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Deal struct {
|
||||||
|
Unixfs0 *Unixfs0Offer
|
||||||
|
}
|
||||||
|
|
||||||
|
type AcceptedResponse struct{}
|
||||||
|
type RejectedResponse struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
type ErrorResponse RejectedResponse
|
||||||
|
|
||||||
|
type DealResponse struct {
|
||||||
|
*AcceptedResponse
|
||||||
|
*RejectedResponse
|
||||||
|
*ErrorResponse
|
||||||
|
}
|
||||||
|
19
retrieval/verify.go
Normal file
19
retrieval/verify.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package retrieval
|
||||||
|
|
||||||
|
import blocks "github.com/ipfs/go-block-format"
|
||||||
|
|
||||||
|
type BlockVerifier interface {
|
||||||
|
Verify(blocks.Block) (internal bool, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: BalancedUnixFs0Verifier
|
||||||
|
|
||||||
|
type OptimisticVerifier struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OptimisticVerifier) Verify(blocks.Block) (bool, error) {
|
||||||
|
// It's probably fine
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ BlockVerifier = &OptimisticVerifier{}
|
Loading…
Reference in New Issue
Block a user