diff --git a/go.mod b/go.mod index 72a21bec5..c6e65e25e 100644 --- a/go.mod +++ b/go.mod @@ -50,6 +50,7 @@ require ( go.uber.org/dig v1.7.0 // indirect go.uber.org/fx v1.9.0 go.uber.org/goleak v0.10.0 // indirect + golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 gopkg.in/urfave/cli.v2 v2.0.0-20180128182452-d3ae77c26ac8 launchpad.net/gocheck v0.0.0-20140225173054-000000000087 // indirect ) diff --git a/node/repo/interface.go b/node/repo/interface.go new file mode 100644 index 000000000..4c9823ab9 --- /dev/null +++ b/node/repo/interface.go @@ -0,0 +1,45 @@ +package repo + +import ( + "github.com/ipfs/go-datastore" + "github.com/libp2p/go-libp2p-core/crypto" + "github.com/multiformats/go-multiaddr" + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-lotus/node/config" +) + +var ( + ErrNoAPIEndpoint = xerrors.New("no API Endpoint set") + ErrRepoAlreadyLocked = xerrors.New("repo is already locked") + ErrClosedRepo = xerrors.New("repo is no longer open") +) + +type Repo interface { + // APIEndpoint returns multiaddress for communication with Lotus API + APIEndpoint() (multiaddr.Multiaddr, error) + + // Lock locks the repo for exclusive use. + Lock() (LockedRepo, error) +} + +type LockedRepo interface { + // Close closes repo and removes lock. + Close() error + + // Returns datastore defined in this repo. + Datastore() (datastore.Datastore, error) + + // Returns config in this repo + Config() (*config.Root, error) + + // Libp2pIdentity returns private key for libp2p indentity + Libp2pIdentity() (crypto.PrivKey, error) + + // SetAPIEndpoint sets the endpoint of the current API + // so it can be read by API clients + SetAPIEndpoint(multiaddr.Multiaddr) error + + // Wallet returns store of private keys for Filecoin transactions + Wallet() (interface{}, error) +} diff --git a/node/repo/memrepo.go b/node/repo/memrepo.go new file mode 100644 index 000000000..de2ace933 --- /dev/null +++ b/node/repo/memrepo.go @@ -0,0 +1,166 @@ +package repo + +import ( + "crypto/rand" + "sync" + + "github.com/ipfs/go-datastore" + dssync "github.com/ipfs/go-datastore/sync" + "github.com/libp2p/go-libp2p-core/crypto" + "github.com/multiformats/go-multiaddr" + + "github.com/filecoin-project/go-lotus/node/config" +) + +type MemRepo struct { + api struct { + sync.Mutex + ma multiaddr.Multiaddr + } + + repoLock chan struct{} + token *byte + + datastore datastore.Datastore + configF func() *config.Root + libp2pKey crypto.PrivKey + wallet interface{} +} + +type lockedMemRepo struct { + mem *MemRepo + sync.RWMutex + + token *byte +} + +var _ Repo = &MemRepo{} + +// MemRepoOptions contains options for memory repo +type MemRepoOptions struct { + Ds datastore.Datastore + ConfigF func() *config.Root + Libp2pKey crypto.PrivKey + Wallet interface{} +} + +// NewMemory creates new memory based repo with provided options. +// opts can be nil, it will be replaced with defaults. +// Any field in opts can be nil, they will be replaced by defaults. +func NewMemory(opts *MemRepoOptions) *MemRepo { + if opts == nil { + opts = &MemRepoOptions{} + } + if opts.ConfigF == nil { + opts.ConfigF = config.Default + } + if opts.Ds == nil { + opts.Ds = dssync.MutexWrap(datastore.NewMapDatastore()) + } + if opts.Libp2pKey == nil { + pk, _, err := crypto.GenerateEd25519Key(rand.Reader) + if err != nil { + panic(err) + } + opts.Libp2pKey = pk + } + + return &MemRepo{ + repoLock: make(chan struct{}, 1), + + datastore: opts.Ds, + configF: opts.ConfigF, + libp2pKey: opts.Libp2pKey, + wallet: opts.Wallet, + } +} + +func (mem *MemRepo) APIEndpoint() (multiaddr.Multiaddr, error) { + mem.api.Lock() + defer mem.api.Unlock() + if mem.api.ma == nil { + return nil, ErrNoAPIEndpoint + } + return mem.api.ma, nil +} + +func (mem *MemRepo) Lock() (LockedRepo, error) { + select { + case mem.repoLock <- struct{}{}: + default: + return nil, ErrRepoAlreadyLocked + } + mem.token = new(byte) + + return &lockedMemRepo{ + mem: mem, + token: mem.token, + }, nil +} + +func (lmem *lockedMemRepo) checkToken() error { + lmem.RLock() + defer lmem.RUnlock() + if lmem.mem.token != lmem.token { + return ErrClosedRepo + } + return nil +} + +func (lmem *lockedMemRepo) Close() error { + if err := lmem.checkToken(); err != nil { + return err + } + lmem.Lock() + defer lmem.Unlock() + + if lmem.mem.token != lmem.token { + return ErrClosedRepo + } + + lmem.mem.token = nil + lmem.mem.api.Lock() + lmem.mem.api.ma = nil + lmem.mem.api.Unlock() + <-lmem.mem.repoLock // unlock + return nil + +} + +func (lmem *lockedMemRepo) Datastore() (datastore.Datastore, error) { + if err := lmem.checkToken(); err != nil { + return nil, err + } + return lmem.mem.datastore, nil +} + +func (lmem *lockedMemRepo) Config() (*config.Root, error) { + if err := lmem.checkToken(); err != nil { + return nil, err + } + return lmem.mem.configF(), nil +} + +func (lmem *lockedMemRepo) Libp2pIdentity() (crypto.PrivKey, error) { + if err := lmem.checkToken(); err != nil { + return nil, err + } + return lmem.mem.libp2pKey, nil +} + +func (lmem *lockedMemRepo) SetAPIEndpoint(ma multiaddr.Multiaddr) error { + if err := lmem.checkToken(); err != nil { + return err + } + lmem.mem.api.Lock() + lmem.mem.api.ma = ma + lmem.mem.api.Unlock() + return nil +} + +func (lmem *lockedMemRepo) Wallet() (interface{}, error) { + if err := lmem.checkToken(); err != nil { + return nil, err + } + return lmem.mem.wallet, nil +} diff --git a/node/repo/memrepo_test.go b/node/repo/memrepo_test.go new file mode 100644 index 000000000..27b6d9f4b --- /dev/null +++ b/node/repo/memrepo_test.go @@ -0,0 +1,57 @@ +package repo + +import ( + "testing" + + "github.com/multiformats/go-multiaddr" + "github.com/stretchr/testify/assert" +) + +func TestMemRepo(t *testing.T) { + repo := NewMemory(nil) + apima, err := repo.APIEndpoint() + if assert.Error(t, err) { + assert.Equal(t, ErrNoAPIEndpoint, err) + } + assert.Nil(t, apima, "with no api endpoint, return should be nil") + + lrepo, err := repo.Lock() + assert.NoError(t, err, "should be able to lock once") + assert.NotNil(t, lrepo, "locked repo shouldn't be nil") + + { + lrepo2, err := repo.Lock() + if assert.Error(t, err) { + assert.Equal(t, ErrRepoAlreadyLocked, err) + } + assert.Nil(t, lrepo2, "with locked repo errors, nil should be returned") + } + + err = lrepo.Close() + assert.NoError(t, err, "should be able to unlock") + + lrepo, err = repo.Lock() + assert.NoError(t, err, "should be able to relock") + assert.NotNil(t, lrepo, "locked repo shouldn't be nil") + + ma, err := multiaddr.NewMultiaddr("/ip4/127.0.0.1/tcp/43244") + assert.NoError(t, err, "creating multiaddr shouldn't error") + + err = lrepo.SetAPIEndpoint(ma) + assert.NoError(t, err, "setting multiaddr shouldn't error") + + apima, err = repo.APIEndpoint() + assert.NoError(t, err, "setting multiaddr shouldn't error") + assert.Equal(t, ma, apima, "returned API multiaddr should be the same") + + err = lrepo.Close() + assert.NoError(t, err, "should be able to close") + + apima, err = repo.APIEndpoint() + + if assert.Error(t, err) { + assert.Equal(t, ErrNoAPIEndpoint, err, "after closing repo, api should be nil") + } + assert.Nil(t, apima, "with closed repo, apima should be set back to nil") + +}