Skip to content
Snippets Groups Projects
Commit 844d650e authored by capossele's avatar capossele
Browse files

:construction: WIP

parent 93e83505
No related branches found
No related tags found
No related merge requests found
Showing
with 1511 additions and 2 deletions
package fpctest
import (
"context"
"flag"
"fmt"
"net"
"strconv"
"sync"
"github.com/iotaledger/goshimmer/dapps/fpctest/packages/tangle"
"github.com/iotaledger/goshimmer/packages/prng"
"github.com/iotaledger/goshimmer/packages/shutdown"
"github.com/iotaledger/goshimmer/packages/vote"
"github.com/iotaledger/goshimmer/packages/vote/fpc"
votenet "github.com/iotaledger/goshimmer/packages/vote/net"
"github.com/iotaledger/goshimmer/plugins/autopeering"
"github.com/iotaledger/goshimmer/plugins/autopeering/local"
"github.com/iotaledger/goshimmer/plugins/config"
"github.com/iotaledger/hive.go/autopeering/peer"
"github.com/iotaledger/hive.go/autopeering/peer/service"
"github.com/iotaledger/hive.go/daemon"
"github.com/iotaledger/hive.go/events"
"github.com/iotaledger/hive.go/logger"
"google.golang.org/grpc"
)
const (
// CfgFPCQuerySampleSize defines how many nodes will be queried each round.
CfgFPCQuerySampleSize = "fpctest.querySampleSize"
// CfgFPCRoundInterval defines how long a round lasts (in seconds)
CfgFPCRoundInterval = "fpctest.roundInterval"
// CfgFPCBindAddress defines on which address the FPC service should listen.
CfgFPCBindAddress = "fpctest.bindAddress"
)
func init() {
flag.Int(CfgFPCQuerySampleSize, 3, "Size of the voting quorum (k)")
flag.Int(CfgFPCRoundInterval, 5, "FPC round interval [s]")
flag.String(CfgFPCBindAddress, "0.0.0.0:10895", "the bind address on which the FPC vote server binds to")
}
var (
voter *fpc.FPC
voterOnce sync.Once
voterServer *votenet.VoterServer
roundIntervalSeconds int64 = 5
)
// Voter returns the DRNGRoundBasedVoter instance used by the FPC plugin.
func Voter() vote.DRNGRoundBasedVoter {
voterOnce.Do(func() {
// create a function which gets OpinionGivers
opinionGiverFunc := func() (givers []vote.OpinionGiver, err error) {
opinionGivers := make([]vote.OpinionGiver, 0)
for _, p := range autopeering.Discovery().GetVerifiedPeers() {
fpcService := p.Services().Get(service.FPCKey)
if fpcService == nil {
continue
}
// TODO: maybe cache the PeerOpinionGiver instead of creating a new one every time
opinionGivers = append(opinionGivers, &PeerOpinionGiver{p: p})
}
return opinionGivers, nil
}
voter = fpc.New(opinionGiverFunc)
})
return voter
}
func configureFPC() {
log = logger.NewLogger(PluginName)
lPeer := local.GetInstance()
bindAddr := config.Node.GetString(CfgFPCBindAddress)
_, portStr, err := net.SplitHostPort(bindAddr)
if err != nil {
log.Fatalf("FPC bind address '%s' is invalid: %s", bindAddr, err)
}
port, err := strconv.Atoi(portStr)
if err != nil {
log.Fatalf("FPC bind address '%s' is invalid: %s", bindAddr, err)
}
if err := lPeer.UpdateService(service.FPCKey, "tcp", port); err != nil {
log.Fatalf("could not update services: %v", err)
}
Voter()
}
func runFPC() {
daemon.BackgroundWorker("FPCTestVoterServer", func(shutdownSignal <-chan struct{}) {
voterServer = votenet.New(voter, func(id string) vote.Opinion {
ID, err := tangle.IDFromBase58(id)
if err != nil {
log.Errorf("received invalid vote request for conflict '%s'", id)
return vote.Unknown
}
cachedMetadata := FPCTangle.PayloadMetadata(ID)
defer cachedMetadata.Release()
metadata := cachedMetadata.Unwrap()
if metadata == nil {
return vote.Unknown
}
if !metadata.IsLiked() {
return vote.Dislike
}
return vote.Like
}, config.Node.GetString(CfgFPCBindAddress))
go func() {
if err := voterServer.Run(); err != nil {
log.Error(err)
}
}()
log.Infof("Started vote server on %s", config.Node.GetString(CfgFPCBindAddress))
<-shutdownSignal
voterServer.Shutdown()
log.Info("Stopped vote server")
}, shutdown.PriorityFPC)
daemon.BackgroundWorker("FPCTestRoundsInitiator", func(shutdownSignal <-chan struct{}) {
log.Infof("Started FPC round initiator")
unixTsPRNG := prng.NewUnixTimestampPRNG(roundIntervalSeconds)
unixTsPRNG.Start()
defer unixTsPRNG.Stop()
exit:
for {
select {
case r := <-unixTsPRNG.C():
if err := voter.Round(r); err != nil {
log.Errorf("unable to execute FPC round: %s", err)
}
case <-shutdownSignal:
break exit
}
}
log.Infof("Stopped FPC round initiator")
}, shutdown.PriorityFPC)
voter.Events().RoundExecuted.Attach(events.NewClosure(func(roundStats *vote.RoundStats) {
peersQueried := len(roundStats.QueriedOpinions)
voteContextsCount := len(roundStats.ActiveVoteContexts)
log.Infof("executed round with rand %0.4f for %d vote contexts on %d peers, took %v", roundStats.RandUsed, voteContextsCount, peersQueried, roundStats.Duration)
}))
}
// PeerOpinionGiver implements the OpinionGiver interface based on a peer.
type PeerOpinionGiver struct {
p *peer.Peer
}
// Query queries another node for its opinion.
func (pog *PeerOpinionGiver) Query(ctx context.Context, ids []string) (vote.Opinions, error) {
fpcServicePort := pog.p.Services().Get(service.FPCKey).Port()
fpcAddr := net.JoinHostPort(pog.p.IP().String(), strconv.Itoa(fpcServicePort))
var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
// connect to the FPC service
conn, err := grpc.Dial(fpcAddr, opts...)
if err != nil {
return nil, fmt.Errorf("unable to connect to FPC service: %w", err)
}
defer conn.Close()
client := votenet.NewVoterQueryClient(conn)
reply, err := client.Opinion(ctx, &votenet.QueryRequest{Id: ids})
if err != nil {
return nil, fmt.Errorf("unable to query opinions: %w", err)
}
// convert int32s in reply to opinions
opinions := make(vote.Opinions, len(reply.Opinion))
for i, intOpn := range reply.Opinion {
opinions[i] = vote.ConvertInt32Opinion(intOpn)
}
return opinions, nil
}
// ID returns a string representation of the identifier of the underlying Peer.
func (pog *PeerOpinionGiver) ID() string {
return pog.p.ID().String()
}
package payload
import (
"crypto/rand"
"fmt"
"github.com/iotaledger/hive.go/marshalutil"
"github.com/mr-tron/base58"
)
// ID represents the hash of a payload that is used to identify the given payload.
type ID [IDLength]byte
// NewID creates a payload id from a base58 encoded string.
func NewID(base58EncodedString string) (result ID, err error) {
bytes, err := base58.Decode(base58EncodedString)
if err != nil {
return
}
if len(bytes) != IDLength {
err = fmt.Errorf("length of base58 formatted payload id is wrong")
return
}
copy(result[:], bytes)
return
}
// ParseID is a wrapper for simplified unmarshaling in a byte stream using the marshalUtil package.
func ParseID(marshalUtil *marshalutil.MarshalUtil) (ID, error) {
id, err := marshalUtil.Parse(func(data []byte) (interface{}, int, error) { return IDFromBytes(data) })
if err != nil {
return ID{}, err
}
return id.(ID), nil
}
// IDFromBytes unmarshals a payload id from a sequence of bytes.
// It either creates a new payload id or fills the optionally provided object with the parsed information.
func IDFromBytes(bytes []byte, optionalTargetObject ...*ID) (result ID, consumedBytes int, err error) {
// determine the target object that will hold the unmarshaled information
var targetObject *ID
switch len(optionalTargetObject) {
case 0:
targetObject = &result
case 1:
targetObject = optionalTargetObject[0]
default:
panic("too many arguments in call to IDFromBytes")
}
// initialize helper
marshalUtil := marshalutil.New(bytes)
// read id from bytes
idBytes, err := marshalUtil.ReadBytes(IDLength)
if err != nil {
return
}
copy(targetObject[:], idBytes)
// copy result if we have provided a target object
result = *targetObject
// return the number of bytes we processed
consumedBytes = marshalUtil.ReadOffset()
return
}
// RandomID creates a random payload id which can for example be used in unit tests.
func RandomID() (id ID) {
// generate a random sequence of bytes
idBytes := make([]byte, IDLength)
if _, err := rand.Read(idBytes); err != nil {
panic(err)
}
// copy the generated bytes into the result
copy(id[:], idBytes)
return
}
// String returns a base58 encoded version of the payload id.
func (id ID) String() string {
return base58.Encode(id[:])
}
// Bytes returns a marshaled version of this ID.
func (id ID) Bytes() []byte {
return id[:]
}
// GenesisID contains the zero value of this ID which represents the genesis.
var GenesisID ID
// IDLength defined the amount of bytes in a payload id (32 bytes hash value).
const IDLength = 32
package payload
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test(t *testing.T) {
// create variable for id
sourceID, err := NewID("4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM")
if err != nil {
panic(err)
}
// read serialized id into both variables
var restoredIDPointer ID
restoredIDValue, _, err := IDFromBytes(sourceID.Bytes(), &restoredIDPointer)
if err != nil {
panic(err)
}
// check if both variables give the same result
assert.Equal(t, sourceID, restoredIDValue)
assert.Equal(t, sourceID, restoredIDPointer)
}
package payload
import (
"sync"
"github.com/iotaledger/goshimmer/packages/binary/messagelayer/payload"
"github.com/iotaledger/hive.go/marshalutil"
"github.com/iotaledger/hive.go/objectstorage"
"github.com/iotaledger/hive.go/stringify"
"golang.org/x/crypto/blake2b"
)
// NonceSize is the size of the nonce.
const NonceSize = 32
// Payload represents the entity that forms the Tangle by referencing other Payloads using their trunk and branch.
// A Payload contains a transaction and defines, where in the Tangle a transaction is attached.
type Payload struct {
objectstorage.StorableObjectFlags
id *ID
idMutex sync.RWMutex
nonce []byte
like uint32
bytes []byte
bytesMutex sync.RWMutex
}
// New is the constructor of a Payload and creates a new Payload object from the given details.
func New(like uint32, nonce []byte) *Payload {
if len(nonce) < NonceSize {
return nil
}
p := &Payload{
like: like,
nonce: make([]byte, NonceSize),
}
copy(p.nonce, nonce[:NonceSize])
return p
}
// FromBytes parses the marshaled version of a Payload into an object.
// It either returns a new Payload or fills an optionally provided Payload with the parsed information.
func FromBytes(bytes []byte, optionalTargetObject ...*Payload) (result *Payload, consumedBytes int, err error) {
marshalUtil := marshalutil.New(bytes)
result, err = Parse(marshalUtil, optionalTargetObject...)
consumedBytes = marshalUtil.ReadOffset()
return
}
// FromStorageKey is a factory method that creates a new Payload instance from a storage key of the objectstorage.
// It is used by the objectstorage, to create new instances of this entity.
func FromStorageKey(key []byte, optionalTargetObject ...*Payload) (result *Payload, consumedBytes int, err error) {
// determine the target object that will hold the unmarshaled information
switch len(optionalTargetObject) {
case 0:
result = &Payload{}
case 1:
result = optionalTargetObject[0]
default:
panic("too many arguments in call to MissingPayloadFromStorageKey")
}
// parse the properties that are stored in the key
marshalUtil := marshalutil.New(key)
payloadID, idErr := ParseID(marshalUtil)
if idErr != nil {
err = idErr
return
}
result.id = &payloadID
consumedBytes = marshalUtil.ReadOffset()
return
}
// Parse unmarshals a Payload using the given marshalUtil (for easier marshaling/unmarshaling).
func Parse(marshalUtil *marshalutil.MarshalUtil, optionalTargetObject ...*Payload) (result *Payload, err error) {
// determine the target object that will hold the unmarshaled information
switch len(optionalTargetObject) {
case 0:
result = &Payload{}
case 1:
result = optionalTargetObject[0]
default:
panic("too many arguments in call to Parse")
}
_, err = marshalUtil.Parse(func(data []byte) (parseResult interface{}, parsedBytes int, parseErr error) {
parsedBytes, parseErr = result.UnmarshalObjectStorageValue(data)
return
})
return
}
// ID returns the identifier if the Payload.
func (payload *Payload) ID() ID {
// acquire lock for reading id
payload.idMutex.RLock()
// return if id has been calculated already
if payload.id != nil {
defer payload.idMutex.RUnlock()
return *payload.id
}
// switch to write lock
payload.idMutex.RUnlock()
payload.idMutex.Lock()
defer payload.idMutex.Unlock()
// return if id has been calculated in the mean time
if payload.id != nil {
return *payload.id
}
// otherwise calculate the id
marshalUtil := marshalutil.New(NonceSize + marshalutil.UINT32_SIZE)
marshalUtil.WriteBytes(payload.Nonce())
marshalUtil.WriteUint32(payload.Like())
var id ID = blake2b.Sum256(marshalUtil.Bytes())
payload.id = &id
return id
}
// Nonce returns the nonce of the Payload.
func (payload *Payload) Nonce() []byte {
return payload.nonce
}
// Like returns the like of this Payload.
func (payload *Payload) Like() uint32 {
return payload.like
}
// Bytes returns a marshaled version of this Payload.
func (payload *Payload) Bytes() []byte {
return payload.ObjectStorageValue()
}
func (payload *Payload) String() string {
return stringify.Struct("Payload",
stringify.StructField("ID", payload.ID()),
stringify.StructField("nonce", payload.Nonce()),
stringify.StructField("like", payload.Like()),
)
}
// region Payload implementation ///////////////////////////////////////////////////////////////////////////////////////
// Type represents the identifier which addresses the value Payload type.
const Type = payload.Type(10895)
// Type returns the type of the Payload.
func (payload *Payload) Type() payload.Type {
return Type
}
// ObjectStorageValue returns the bytes that represent all remaining information (not stored in the key) of a marshaled
// Branch.
func (payload *Payload) ObjectStorageValue() (bytes []byte) {
// acquire lock for reading bytes
payload.bytesMutex.RLock()
// return if bytes have been determined already
if bytes = payload.bytes; bytes != nil {
defer payload.bytesMutex.RUnlock()
return
}
// switch to write lock
payload.bytesMutex.RUnlock()
payload.bytesMutex.Lock()
defer payload.bytesMutex.Unlock()
// return if bytes have been determined in the mean time
if bytes = payload.bytes; bytes != nil {
return
}
// marshal fields
payloadLength := NonceSize + marshalutil.UINT32_SIZE
marshalUtil := marshalutil.New(marshalutil.UINT32_SIZE + marshalutil.UINT32_SIZE + payloadLength)
marshalUtil.WriteUint32(Type)
marshalUtil.WriteUint32(uint32(payloadLength))
marshalUtil.WriteBytes(payload.Nonce())
marshalUtil.WriteUint32(payload.Like())
bytes = marshalUtil.Bytes()
// store result
payload.bytes = bytes
return
}
// UnmarshalObjectStorageValue unmarshals the bytes that are stored in the value of the objectstorage.
func (payload *Payload) UnmarshalObjectStorageValue(data []byte) (consumedBytes int, err error) {
marshalUtil := marshalutil.New(data)
// read information that are required to identify the payload from the outside
_, err = marshalUtil.ReadUint32()
if err != nil {
return
}
_, err = marshalUtil.ReadUint32()
if err != nil {
return
}
// parse payload
if payload.nonce, err = marshalUtil.ReadBytes(NonceSize); err != nil {
return
}
if payload.like, err = marshalUtil.ReadUint32(); err != nil {
return
}
// return the number of bytes we processed
consumedBytes = marshalUtil.ReadOffset()
// store bytes, so we don't have to marshal manually
payload.bytes = make([]byte, consumedBytes)
copy(payload.bytes, data[:consumedBytes])
return
}
// Unmarshal unmarshals a given slice of bytes and fills the object with the.
func (payload *Payload) Unmarshal(data []byte) (err error) {
_, _, err = FromBytes(data, payload)
return
}
func init() {
payload.RegisterType(Type, func(data []byte) (payload payload.Payload, err error) {
payload, _, err = FromBytes(data)
return
})
}
// define contract (ensure that the struct fulfills the corresponding interface)
var _ payload.Payload = &Payload{}
// endregion ///////////////////////////////////////////////////////////////////////////////////////////////////////////
// region StorableObject implementation ////////////////////////////////////////////////////////////////////////////////
// ObjectStorageKey returns the bytes that are used a key when storing the Branch in an objectstorage.
func (payload *Payload) ObjectStorageKey() []byte {
return payload.ID().Bytes()
}
// Update is disabled but needs to be implemented to be compatible with the objectstorage.
func (payload *Payload) Update(other objectstorage.StorableObject) {
panic("a Payload should never be updated")
}
// define contract (ensure that the struct fulfills the corresponding interface)
var _ objectstorage.StorableObject = &Payload{}
// endregion ///////////////////////////////////////////////////////////////////////////////////////////////////////////
// CachedPayload is a wrapper for the object storage, that takes care of type casting the managed objects.
// Since go does not have generics (yet), the object storage works based on the generic "interface{}" type, which means
// that we have to regularly type cast the returned objects, to match the expected type. To reduce the burden of
// manually managing these type, we create a wrapper that does this for us. This way, we can consistently handle the
// specialized types of CachedObjects, without having to manually type cast over and over again.
type CachedPayload struct {
objectstorage.CachedObject
}
// Retain wraps the underlying method to return a new "wrapped object".
func (cachedPayload *CachedPayload) Retain() *CachedPayload {
return &CachedPayload{cachedPayload.CachedObject.Retain()}
}
// Consume wraps the underlying method to return the correctly typed objects in the callback.
func (cachedPayload *CachedPayload) Consume(consumer func(payload *Payload)) bool {
return cachedPayload.CachedObject.Consume(func(object objectstorage.StorableObject) {
consumer(object.(*Payload))
})
}
// Unwrap provides a way to "Get" a type casted version of the underlying object.
func (cachedPayload *CachedPayload) Unwrap() *Payload {
untypedTransaction := cachedPayload.Get()
if untypedTransaction == nil {
return nil
}
typeCastedTransaction := untypedTransaction.(*Payload)
if typeCastedTransaction == nil || typeCastedTransaction.IsDeleted() {
return nil
}
return typeCastedTransaction
}
package payload
import (
"crypto/rand"
"fmt"
"testing"
"time"
"github.com/iotaledger/goshimmer/packages/binary/messagelayer/message"
"github.com/iotaledger/hive.go/identity"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func ExamplePayload() {
nonce := make([]byte, NonceSize)
rand.Read(nonce)
fpcTestPayload := New(100, nonce)
localIdentity := identity.GenerateLocalIdentity()
msg := message.New(
// trunk in "network tangle" ontology (filled by tipSelector)
message.EmptyId,
// branch in "network tangle" ontology (filled by tipSelector)
message.EmptyId,
// issuer of the transaction (signs automatically)
localIdentity,
// the time when the transaction was created
time.Now(),
// the ever increasing sequence number of this transaction
0,
// payload
fpcTestPayload,
)
fmt.Println(msg)
}
func TestPayload(t *testing.T) {
nonce := make([]byte, NonceSize)
_, err := rand.Read(nonce)
require.NoError(t, err)
originalPayload := New(100, nonce)
clonedPayload1, _, err := FromBytes(originalPayload.Bytes())
if err != nil {
panic(err)
}
assert.Equal(t, originalPayload.ID(), clonedPayload1.ID())
assert.Equal(t, originalPayload.Nonce(), clonedPayload1.Nonce())
assert.Equal(t, originalPayload.Like(), clonedPayload1.Like())
assert.Equal(t, uint32(100), clonedPayload1.Like())
}
package tangle
import (
"github.com/iotaledger/goshimmer/dapps/fpctest/packages/payload"
"github.com/iotaledger/hive.go/events"
)
// Events is a container for the different kind of events of the Tangle.
type Events struct {
// Get's called whenever a transaction
PayloadAttached *events.Event
}
func newEvents() *Events {
return &Events{
PayloadAttached: events.NewEvent(cachedPayloadEvent),
}
}
func payloadIDEvent(handler interface{}, params ...interface{}) {
handler.(func(payload.ID))(params[0].(payload.ID))
}
func cachedPayloadEvent(handler interface{}, params ...interface{}) {
handler.(func(*payload.CachedPayload, *CachedPayloadMetadata))(
params[0].(*payload.CachedPayload).Retain(),
params[1].(*CachedPayloadMetadata).Retain(),
)
}
package tangle
import (
"github.com/iotaledger/goshimmer/dapps/fpctest/packages/payload"
"github.com/iotaledger/hive.go/objectstorage"
)
const (
// the following values are a list of prefixes defined as an enum
_ byte = iota
// prefixes used for the objectstorage
osPayload
osPayloadMetadata
)
func osPayloadFactory(key []byte) (objectstorage.StorableObject, int, error) {
return payload.FromStorageKey(key)
}
func osPayloadMetadataFactory(key []byte) (objectstorage.StorableObject, int, error) {
return PayloadMetadataFromStorageKey(key)
}
package tangle
import (
"sync"
"github.com/iotaledger/goshimmer/dapps/fpctest/packages/payload"
"github.com/iotaledger/hive.go/marshalutil"
"github.com/iotaledger/hive.go/objectstorage"
"github.com/iotaledger/hive.go/stringify"
)
// PayloadMetadata is a container for the metadata of a value transfer payload.
// It is used to store the information in the database.
type PayloadMetadata struct {
objectstorage.StorableObjectFlags
payloadID payload.ID
liked bool
likedMutex sync.RWMutex
}
// NewPayloadMetadata creates an empty container for the metadata of a value transfer payload.
func NewPayloadMetadata(payloadID payload.ID) *PayloadMetadata {
return &PayloadMetadata{
payloadID: payloadID,
}
}
// PayloadMetadataFromBytes unmarshals a container with the metadata of a value transfer payload from a sequence of bytes.
// It either creates a new container or fills the optionally provided container with the parsed information.
func PayloadMetadataFromBytes(bytes []byte, optionalTargetObject ...*PayloadMetadata) (result *PayloadMetadata, consumedBytes int, err error) {
marshalUtil := marshalutil.New(bytes)
result, err = ParsePayloadMetadata(marshalUtil, optionalTargetObject...)
consumedBytes = marshalUtil.ReadOffset()
return
}
// ParsePayloadMetadata is a wrapper for simplified unmarshaling in a byte stream using the marshalUtil package.
func ParsePayloadMetadata(marshalUtil *marshalutil.MarshalUtil, optionalTargetObject ...*PayloadMetadata) (result *PayloadMetadata, err error) {
parsedObject, parseErr := marshalUtil.Parse(func(data []byte) (interface{}, int, error) {
return PayloadMetadataFromStorageKey(data, optionalTargetObject...)
})
if parseErr != nil {
err = parseErr
return
}
result = parsedObject.(*PayloadMetadata)
_, err = marshalUtil.Parse(func(data []byte) (parseResult interface{}, parsedBytes int, parseErr error) {
parsedBytes, parseErr = result.UnmarshalObjectStorageValue(data)
return
})
return
}
// PayloadMetadataFromStorageKey gets called when we restore transaction metadata from the storage. The bytes and the content will be
// unmarshaled by an external caller using the binary.ObjectStorageValue interface.
func PayloadMetadataFromStorageKey(id []byte, optionalTargetObject ...*PayloadMetadata) (result *PayloadMetadata, consumedBytes int, err error) {
// determine the target object that will hold the unmarshaled information
switch len(optionalTargetObject) {
case 0:
result = &PayloadMetadata{}
case 1:
result = optionalTargetObject[0]
default:
panic("too many arguments in call to PayloadMetadataFromStorageKey")
}
// parse the properties that are stored in the key
marshalUtil := marshalutil.New(id)
if result.payloadID, err = payload.ParseID(marshalUtil); err != nil {
return
}
consumedBytes = marshalUtil.ReadOffset()
return
}
// PayloadID return the id of the payload that this metadata is associated to.
func (payloadMetadata *PayloadMetadata) PayloadID() payload.ID {
return payloadMetadata.payloadID
}
// IsLiked returns true if the payload has been marked as liked.
func (payloadMetadata *PayloadMetadata) IsLiked() (result bool) {
payloadMetadata.likedMutex.RLock()
result = payloadMetadata.liked
payloadMetadata.likedMutex.RUnlock()
return
}
// SetSolid marks a payload as either solid or not solid.
// It returns true if the solid flag was changes and automatically updates the solidificationTime as well.
func (payloadMetadata *PayloadMetadata) SetLike(liked bool) (modified bool) {
payloadMetadata.likedMutex.RLock()
if payloadMetadata.liked != liked {
payloadMetadata.likedMutex.RUnlock()
payloadMetadata.likedMutex.Lock()
if payloadMetadata.liked != liked {
payloadMetadata.liked = liked
// if solid {
// payloadMetadata.solidificationTimeMutex.Lock()
// payloadMetadata.solidificationTime = time.Now()
// payloadMetadata.solidificationTimeMutex.Unlock()
// }
payloadMetadata.SetModified()
modified = true
}
payloadMetadata.likedMutex.Unlock()
} else {
payloadMetadata.likedMutex.RUnlock()
}
return
}
// // SoldificationTime returns the time when the payload was marked to be solid.
// func (payloadMetadata *PayloadMetadata) SoldificationTime() time.Time {
// payloadMetadata.solidificationTimeMutex.RLock()
// defer payloadMetadata.solidificationTimeMutex.RUnlock()
// return payloadMetadata.solidificationTime
// }
// Bytes marshals the metadata into a sequence of bytes.
func (payloadMetadata *PayloadMetadata) Bytes() []byte {
return marshalutil.New(payload.IDLength + marshalutil.BOOL_SIZE).
WriteBytes(payloadMetadata.ObjectStorageKey()).
WriteBytes(payloadMetadata.ObjectStorageValue()).
Bytes()
}
// String creates a human readable version of the metadata (for debug purposes).
func (payloadMetadata *PayloadMetadata) String() string {
return stringify.Struct("PayloadMetadata",
stringify.StructField("payloadId", payloadMetadata.PayloadID()),
stringify.StructField("liked", payloadMetadata.IsLiked()),
)
}
// ObjectStorageKey returns the key that is used to store the object in the database.
// It is required to match StorableObject interface.
func (payloadMetadata *PayloadMetadata) ObjectStorageKey() []byte {
return payloadMetadata.payloadID.Bytes()
}
// Update is disabled and panics if it ever gets called - updates are supposed to happen through the setters.
// It is required to match StorableObject interface.
func (payloadMetadata *PayloadMetadata) Update(other objectstorage.StorableObject) {
panic("update forbidden")
}
// ObjectStorageValue is required to match the encoding.BinaryMarshaler interface.
func (payloadMetadata *PayloadMetadata) ObjectStorageValue() []byte {
return marshalutil.New(marshalutil.BOOL_SIZE).
WriteBool(payloadMetadata.liked).
Bytes()
}
// UnmarshalObjectStorageValue is required to match the encoding.BinaryUnmarshaler interface.
func (payloadMetadata *PayloadMetadata) UnmarshalObjectStorageValue(data []byte) (consumedBytes int, err error) {
marshalUtil := marshalutil.New(data)
if payloadMetadata.liked, err = marshalUtil.ReadBool(); err != nil {
return
}
consumedBytes = marshalUtil.ReadOffset()
return
}
// CachedPayloadMetadata is a wrapper for the object storage, that takes care of type casting the managed objects.
// Since go does not have generics (yet), the object storage works based on the generic "interface{}" type, which means
// that we have to regularly type cast the returned objects, to match the expected type. To reduce the burden of
// manually managing these type, we create a wrapper that does this for us. This way, we can consistently handle the
// specialized types of CachedObjects, without having to manually type cast over and over again.
type CachedPayloadMetadata struct {
objectstorage.CachedObject
}
// Retain wraps the underlying method to return a new "wrapped object".
func (cachedPayloadMetadata *CachedPayloadMetadata) Retain() *CachedPayloadMetadata {
return &CachedPayloadMetadata{cachedPayloadMetadata.CachedObject.Retain()}
}
// Consume wraps the underlying method to return the correctly typed objects in the callback.
func (cachedPayloadMetadata *CachedPayloadMetadata) Consume(consumer func(payload *PayloadMetadata)) bool {
return cachedPayloadMetadata.CachedObject.Consume(func(object objectstorage.StorableObject) {
consumer(object.(*PayloadMetadata))
})
}
// Unwrap provides a way to "Get" a type casted version of the underlying object.
func (cachedPayloadMetadata *CachedPayloadMetadata) Unwrap() *PayloadMetadata {
untypedTransaction := cachedPayloadMetadata.Get()
if untypedTransaction == nil {
return nil
}
typeCastedTransaction := untypedTransaction.(*PayloadMetadata)
if typeCastedTransaction == nil || typeCastedTransaction.IsDeleted() {
return nil
}
return typeCastedTransaction
}
package tangle
import (
"testing"
"github.com/iotaledger/goshimmer/dapps/fpctest/packages/payload"
"github.com/stretchr/testify/assert"
)
func TestMarshalUnmarshal(t *testing.T) {
originalMetadata := NewPayloadMetadata(payload.GenesisID)
clonedMetadata, _, err := PayloadMetadataFromBytes(originalMetadata.Bytes())
if err != nil {
panic(err)
}
assert.Equal(t, originalMetadata.PayloadID(), clonedMetadata.PayloadID())
assert.Equal(t, originalMetadata.IsLiked(), clonedMetadata.IsLiked())
originalMetadata.SetLike(true)
clonedMetadata, _, err = PayloadMetadataFromBytes(originalMetadata.Bytes())
if err != nil {
panic(err)
}
assert.Equal(t, originalMetadata.PayloadID(), clonedMetadata.PayloadID())
assert.Equal(t, originalMetadata.IsLiked(), clonedMetadata.IsLiked())
}
func TestPayloadMetadata_SetLike(t *testing.T) {
originalMetadata := NewPayloadMetadata(payload.GenesisID)
assert.Equal(t, false, originalMetadata.IsLiked())
originalMetadata.SetLike(true)
assert.Equal(t, true, originalMetadata.IsLiked())
}
package tangle
import (
"fmt"
"time"
"github.com/dgraph-io/badger/v2"
"github.com/iotaledger/goshimmer/dapps/fpctest/packages/payload"
"github.com/iotaledger/goshimmer/packages/binary/storageprefix"
"github.com/iotaledger/hive.go/async"
"github.com/iotaledger/hive.go/objectstorage"
"github.com/mr-tron/base58"
)
// Tangle represents the FPC test tangle that consists out of FPCTest payloads.
// It is an independent ontology, that lives inside the tangle.
type Tangle struct {
payloadStorage *objectstorage.ObjectStorage
payloadMetadataStorage *objectstorage.ObjectStorage
Events Events
storePayloadWorkerPool async.WorkerPool
cleanupWorkerPool async.WorkerPool
}
// New is the constructor of a Tangle and creates a new Tangle object from the given details.
func New(badgerInstance *badger.DB) (result *Tangle) {
osFactory := objectstorage.NewFactory(badgerInstance, storageprefix.ValueTransfers)
result = &Tangle{
payloadStorage: osFactory.New(osPayload, osPayloadFactory, objectstorage.CacheTime(time.Second)),
payloadMetadataStorage: osFactory.New(osPayloadMetadata, osPayloadMetadataFactory, objectstorage.CacheTime(time.Second)),
Events: *newEvents(),
}
return
}
// AttachPayload adds a new payload to the value tangle.
func (tangle *Tangle) AttachPayload(payload *payload.Payload) {
tangle.storePayloadWorkerPool.Submit(func() { tangle.storePayloadWorker(payload) })
}
// GetPayload retrieves a payload from the object storage.
func (tangle *Tangle) GetPayload(payloadID payload.ID) *payload.CachedPayload {
return &payload.CachedPayload{CachedObject: tangle.payloadStorage.Load(payloadID.Bytes())}
}
// IDFromBase58 creates a new ID from a base58 encoded string.
func IDFromBase58(base58String string) (ID payload.ID, err error) {
// decode string
bytes, err := base58.Decode(base58String)
if err != nil {
return
}
// sanitize input
if len(bytes) != payload.IDLength {
err = fmt.Errorf("base58 encoded string does not match the length of a FPCTest ID")
return
}
// copy bytes to result
copy(ID[:], bytes)
return
}
// PayloadMetadata retrieves the metadata of a value payload from the object storage.
func (tangle *Tangle) PayloadMetadata(payloadID payload.ID) *CachedPayloadMetadata {
return &CachedPayloadMetadata{CachedObject: tangle.payloadMetadataStorage.Load(payloadID.Bytes())}
}
// Shutdown stops the worker pools and shuts down the object storage instances.
func (tangle *Tangle) Shutdown() *Tangle {
tangle.storePayloadWorkerPool.ShutdownGracefully()
tangle.cleanupWorkerPool.ShutdownGracefully()
tangle.payloadStorage.Shutdown()
tangle.payloadMetadataStorage.Shutdown()
return tangle
}
// Prune resets the database and deletes all objects (for testing or "node resets").
func (tangle *Tangle) Prune() error {
for _, storage := range []*objectstorage.ObjectStorage{
tangle.payloadStorage,
tangle.payloadMetadataStorage,
} {
if err := storage.Prune(); err != nil {
return err
}
}
return nil
}
// storePayloadWorker is the worker function that stores the payload and calls the corresponding storage events.
func (tangle *Tangle) storePayloadWorker(payloadToStore *payload.Payload) {
// store the payload and transaction models
cachedPayload, cachedPayloadMetadata, payloadStored := tangle.storePayload(payloadToStore)
if !payloadStored {
// abort if we have seen the payload already
return
}
tangle.Events.PayloadAttached.Trigger(cachedPayload, cachedPayloadMetadata)
}
func (tangle *Tangle) storePayload(payloadToStore *payload.Payload) (cachedPayload *payload.CachedPayload, cachedMetadata *CachedPayloadMetadata, payloadStored bool) {
storedTransaction, transactionIsNew := tangle.payloadStorage.StoreIfAbsent(payloadToStore)
if !transactionIsNew {
return
}
cachedPayload = &payload.CachedPayload{CachedObject: storedTransaction}
cachedMetadata = &CachedPayloadMetadata{CachedObject: tangle.payloadMetadataStorage.Store(NewPayloadMetadata(payloadToStore.ID()))}
payloadStored = true
return
}
// isPayloadSolid returns true if the given payload is solid. A payload is considered to be solid solid, if it is either
// already marked as solid or if its referenced payloads are marked as solid.
func (tangle *Tangle) isPayloadLiked(payload *payload.Payload, metadata *PayloadMetadata) bool {
if payload == nil || payload.IsDeleted() {
return false
}
if metadata == nil || metadata.IsDeleted() {
return false
}
return metadata.IsLiked()
}
package fpctest
import (
"math/rand"
"time"
fpcTestPayload "github.com/iotaledger/goshimmer/dapps/fpctest/packages/payload"
"github.com/iotaledger/goshimmer/dapps/fpctest/packages/tangle"
"github.com/iotaledger/goshimmer/packages/binary/messagelayer/message"
messageTangle "github.com/iotaledger/goshimmer/packages/binary/messagelayer/tangle"
"github.com/iotaledger/goshimmer/packages/database"
"github.com/iotaledger/goshimmer/packages/shutdown"
"github.com/iotaledger/goshimmer/packages/vote"
"github.com/iotaledger/goshimmer/plugins/messagelayer"
"github.com/iotaledger/hive.go/daemon"
"github.com/iotaledger/hive.go/events"
"github.com/iotaledger/hive.go/logger"
"github.com/iotaledger/hive.go/node"
)
const (
// PluginName contains the human readable name of the plugin.
PluginName = "FPCTest"
// AverageNetworkDelay contains the average time it takes for a network to propagate through gossip.
AverageNetworkDelay = 6 * time.Second
)
var (
// App is the "plugin" instance of the value-transfers application.
App = node.NewPlugin(PluginName, node.Enabled, configure, run)
// FPCTangle is the FPCTest instance.
FPCTangle *tangle.Tangle
// log holds a reference to the logger used by this app.
log *logger.Logger
)
func configure(_ *node.Plugin) {
log = logger.NewLogger(PluginName)
log.Debug("configuring FPCTest")
// create instances
FPCTangle = tangle.New(database.GetBadgerInstance())
// subscribe to message-layer
messagelayer.Tangle.Events.MessageSolid.Attach(events.NewClosure(onReceiveMessageFromMessageLayer))
// setup behavior of package instances
FPCTangle.Events.PayloadAttached.Attach(events.NewClosure(onReceiveMessageFromFPCTest))
configureFPC()
// TODO: DECIDE WHAT WE SHOULD DO IF FPC FAILS -> cry
// voter.Events().Failed.Attach(events.NewClosure(panic))
}
func run(*node.Plugin) {
_ = daemon.BackgroundWorker("FPCTangle", func(shutdownSignal <-chan struct{}) {
<-shutdownSignal
FPCTangle.Shutdown()
}, shutdown.PriorityTangle)
runFPC()
voter.Events().Finalized.Attach(events.NewClosure(func(id string, opinion vote.Opinion) {
ID, err := tangle.IDFromBase58(id)
if err != nil {
log.Error(err)
return
}
cachedMetadata := FPCTangle.PayloadMetadata(ID)
defer cachedMetadata.Release()
metadata := cachedMetadata.Unwrap()
switch opinion {
case vote.Like:
log.Info("Finalized as LIKE: ", ID)
metadata.SetLike(true)
case vote.Dislike:
log.Info("Finalized as DISLIKE: ", ID)
metadata.SetLike(false)
}
}))
voter.Events().RoundExecuted.Attach(events.NewClosure(func(stats *vote.RoundStats) {
log.Info("New Round - ", stats.RandUsed, len(stats.ActiveVoteContexts))
}))
voter.Events().Failed.Attach(events.NewClosure(func(id string, opinion vote.Opinion) {
log.Info("FPC fail - ", id, opinion)
}))
}
func onReceiveMessageFromMessageLayer(cachedMessage *message.CachedMessage, cachedMessageMetadata *messageTangle.CachedMessageMetadata) {
defer cachedMessage.Release()
defer cachedMessageMetadata.Release()
solidMessage := cachedMessage.Unwrap()
if solidMessage == nil {
// TODO: LOG ERROR?
return
}
messagePayload := solidMessage.Payload()
if messagePayload.Type() != fpcTestPayload.Type {
// TODO: LOG ERROR?
return
}
fpcTestPayload, ok := messagePayload.(*fpcTestPayload.Payload)
if !ok {
// TODO: LOG ERROR?
return
}
log.Info("Receive FPCTest Msg - ", fpcTestPayload.ID().String())
FPCTangle.AttachPayload(fpcTestPayload)
}
func onReceiveMessageFromFPCTest(cachedPayload *fpcTestPayload.CachedPayload, cachedMetadata *tangle.CachedPayloadMetadata) {
defer cachedPayload.Release()
defer cachedMetadata.Release()
log.Info("Conflict detected - ", cachedPayload.Unwrap().ID())
r := rand.New(rand.NewSource(time.Now().UnixNano()))
like := r.Intn(100)
switch uint32(like) < cachedPayload.Unwrap().Like() {
case true:
err := voter.Vote(cachedPayload.Unwrap().ID().String(), vote.Like)
if err != nil {
log.Error(err)
}
case false:
err := voter.Vote(cachedPayload.Unwrap().ID().String(), vote.Dislike)
if err != nil {
log.Error(err)
}
}
return
}
...@@ -7,7 +7,6 @@ import ( ...@@ -7,7 +7,6 @@ import (
"github.com/iotaledger/goshimmer/pluginmgr/research" "github.com/iotaledger/goshimmer/pluginmgr/research"
"github.com/iotaledger/goshimmer/pluginmgr/ui" "github.com/iotaledger/goshimmer/pluginmgr/ui"
"github.com/iotaledger/goshimmer/pluginmgr/webapi" "github.com/iotaledger/goshimmer/pluginmgr/webapi"
"github.com/iotaledger/hive.go/node" "github.com/iotaledger/hive.go/node"
) )
......
...@@ -4,6 +4,8 @@ import ( ...@@ -4,6 +4,8 @@ import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"time" "time"
"golang.org/x/crypto/blake2b"
) )
// TimeSourceFunc is a function which gets an understanding of time in seconds resolution back. // TimeSourceFunc is a function which gets an understanding of time in seconds resolution back.
...@@ -67,7 +69,10 @@ func (utrng *UnixTimestampPrng) send() { ...@@ -67,7 +69,10 @@ func (utrng *UnixTimestampPrng) send() {
if err := binary.Write(buf, binary.LittleEndian, timePoint); err != nil { if err := binary.Write(buf, binary.LittleEndian, timePoint); err != nil {
panic(err) panic(err)
} }
pseudoR := float64(binary.BigEndian.Uint64(buf.Bytes()[:8])>>11) / (1 << 53)
h := blake2b.Sum256(buf.Bytes())
pseudoR := float64(binary.BigEndian.Uint64(h[:8])>>11) / (1 << 53)
// skip slow consumers // skip slow consumers
select { select {
case utrng.c <- pseudoR: case utrng.c <- pseudoR:
......
package research package research
import ( import (
"github.com/iotaledger/goshimmer/dapps/fpctest"
analysisclient "github.com/iotaledger/goshimmer/plugins/analysis/client" analysisclient "github.com/iotaledger/goshimmer/plugins/analysis/client"
analysisserver "github.com/iotaledger/goshimmer/plugins/analysis/server" analysisserver "github.com/iotaledger/goshimmer/plugins/analysis/server"
analysiswebinterface "github.com/iotaledger/goshimmer/plugins/analysis/webinterface" analysiswebinterface "github.com/iotaledger/goshimmer/plugins/analysis/webinterface"
...@@ -13,4 +14,5 @@ var PLUGINS = node.Plugins( ...@@ -13,4 +14,5 @@ var PLUGINS = node.Plugins(
analysisserver.Plugin, analysisserver.Plugin,
analysisclient.Plugin, analysisclient.Plugin,
analysiswebinterface.Plugin, analysiswebinterface.Plugin,
fpctest.App,
) )
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"github.com/iotaledger/goshimmer/plugins/webapi/autopeering" "github.com/iotaledger/goshimmer/plugins/webapi/autopeering"
"github.com/iotaledger/goshimmer/plugins/webapi/data" "github.com/iotaledger/goshimmer/plugins/webapi/data"
"github.com/iotaledger/goshimmer/plugins/webapi/drng" "github.com/iotaledger/goshimmer/plugins/webapi/drng"
"github.com/iotaledger/goshimmer/plugins/webapi/fpctest"
"github.com/iotaledger/goshimmer/plugins/webapi/info" "github.com/iotaledger/goshimmer/plugins/webapi/info"
"github.com/iotaledger/goshimmer/plugins/webapi/message" "github.com/iotaledger/goshimmer/plugins/webapi/message"
"github.com/iotaledger/goshimmer/plugins/webapi/spammer" "github.com/iotaledger/goshimmer/plugins/webapi/spammer"
...@@ -21,4 +22,5 @@ var PLUGINS = node.Plugins( ...@@ -21,4 +22,5 @@ var PLUGINS = node.Plugins(
message.Plugin, message.Plugin,
autopeering.Plugin, autopeering.Plugin,
info.Plugin, info.Plugin,
fpctest.Plugin,
) )
// +build !skippackr
// Code generated by github.com/gobuffalo/packr/v2. DO NOT EDIT.
// You can use the "packr clean" command to clean up this,
// and any other packr generated files.
package dashboard
import _ "github.com/iotaledger/goshimmer/plugins/dashboard/packrd"
package dashboard
import (
"time"
"github.com/iotaledger/goshimmer/packages/shutdown"
"github.com/iotaledger/goshimmer/packages/vote"
"github.com/iotaledger/goshimmer/plugins/drng"
"github.com/iotaledger/hive.go/daemon"
"github.com/iotaledger/hive.go/events"
"github.com/iotaledger/hive.go/workerpool"
)
var fpcLiveFeedWorkerCount = 1
var fpcLiveFeedWorkerQueueSize = 50
var fpcLiveFeedWorkerPool *workerpool.WorkerPool
type fpcRoundMsg struct {
ID string `json:"id"`
Opinion int `json:"opinion"`
}
func configureFPCLiveFeed() {
fpcLiveFeedWorkerPool = workerpool.New(func(task workerpool.Task) {
newMsg := task.Param(0).(vote.RoundStats)
for _, conflict := range newMsg.ActiveVoteContexts {
broadcastWsMessage(&wsmsg{MsgTypeDrng, &fpcRoundMsg{
ID: conflict.ID,
Opinion: int(conflict.LastOpinion()),
}})
}
task.Return(nil)
}, workerpool.WorkerCount(fpcLiveFeedWorkerCount), workerpool.QueueSize(fpcLiveFeedWorkerQueueSize))
}
func runFPCLiveFeed() {
daemon.BackgroundWorker("Analysis[FPCUpdater]", func(shutdownSignal <-chan struct{}) {
newMsgRateLimiter := time.NewTicker(time.Second / 10)
defer newMsgRateLimiter.Stop()
notifyNewFPCRound := events.NewClosure(func(message vote.RoundStats) {
select {
case <-newMsgRateLimiter.C:
fpcLiveFeedWorkerPool.TrySubmit(message)
default:
}
})
drng.Instance().Events.Randomness.Attach(notifyNewFPCRound)
fpcLiveFeedWorkerPool.Start()
defer fpcLiveFeedWorkerPool.Stop()
<-shutdownSignal
log.Info("Stopping Analysis[FPCUpdater] ...")
drng.Instance().Events.Randomness.Detach(notifyNewFPCRound)
log.Info("Stopping Analysis[FPCUpdater] ... done")
}, shutdown.PriorityDashboard)
}
package dashboard
import (
"fmt"
"net/http"
"sync"
"github.com/iotaledger/goshimmer/packages/binary/messagelayer/message"
"github.com/iotaledger/goshimmer/plugins/messagelayer"
"github.com/labstack/echo"
)
// ExplorerMessage defines the struct of the ExplorerMessage.
type ExplorerMessage struct {
// ID is the message ID.
ID string `json:"id"`
// Timestamp is the timestamp of the message.
Timestamp uint `json:"timestamp"`
// TrunkMessageId is the Trunk ID of the message.
TrunkMessageID string `json:"trunk_message_id"`
// BranchMessageId is the Branch ID of the message.
BranchMessageID string `json:"branch_message_id"`
// Solid defines the solid status of the message.
Solid bool `json:"solid"`
}
func createExplorerMessage(msg *message.Message) (*ExplorerMessage, error) {
messageID := msg.Id()
messageMetadata := messagelayer.Tangle.MessageMetadata(messageID)
t := &ExplorerMessage{
ID: messageID.String(),
Timestamp: 0,
TrunkMessageID: msg.TrunkId().String(),
BranchMessageID: msg.BranchId().String(),
Solid: messageMetadata.Unwrap().IsSolid(),
}
return t, nil
}
// ExplorerAddress defines the struct of the ExplorerAddress.
type ExplorerAddress struct {
// Messagess hold the list of *ExplorerMessage.
Messages []*ExplorerMessage `json:"message"`
}
// SearchResult defines the struct of the SearchResult.
type SearchResult struct {
// Message is the *ExplorerMessage.
Message *ExplorerMessage `json:"message"`
// Address is the *ExplorerAddress.
Address *ExplorerAddress `json:"address"`
}
func setupExplorerRoutes(routeGroup *echo.Group) {
routeGroup.GET("/message/:id", func(c echo.Context) (err error) {
messageID, err := message.NewId(c.Param("id"))
if err != nil {
return
}
t, err := findMessage(messageID)
if err != nil {
return
}
return c.JSON(http.StatusOK, t)
})
routeGroup.GET("/address/:id", func(c echo.Context) error {
addr, err := findAddress(c.Param("id"))
if err != nil {
return err
}
return c.JSON(http.StatusOK, addr)
})
routeGroup.GET("/search/:search", func(c echo.Context) error {
search := c.Param("search")
result := &SearchResult{}
if len(search) < 81 {
return fmt.Errorf("%w: search ID %s", ErrInvalidParameter, search)
}
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
messageID, err := message.NewId(search)
if err != nil {
return
}
msg, err := findMessage(messageID)
if err == nil {
result.Message = msg
}
}()
go func() {
defer wg.Done()
addr, err := findAddress(search)
if err == nil {
result.Address = addr
}
}()
wg.Wait()
return c.JSON(http.StatusOK, result)
})
}
func findMessage(messageID message.Id) (explorerMsg *ExplorerMessage, err error) {
if !messagelayer.Tangle.Message(messageID).Consume(func(msg *message.Message) {
explorerMsg, err = createExplorerMessage(msg)
}) {
err = fmt.Errorf("%w: message %s", ErrNotFound, messageID.String())
}
return
}
func findAddress(address string) (*ExplorerAddress, error) {
return nil, fmt.Errorf("%w: address %s", ErrNotFound, address)
// TODO: ADD ADDRESS LOOKUPS ONCE THE VALUE TRANSFER ONTOLOGY IS MERGED
}
.vscode
.DS_STORE
node_modules
.module-cache
*.log*
build
dist
\ No newline at end of file
{
"arrowParens": "always",
"semi": true,
"useTabs": false,
"tabWidth": 2,
"bracketSpacing": true,
"singleQuote": true
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment