diff --git a/packages/batchworkerpool/batchworkerpool.go b/packages/batchworkerpool/batchworkerpool.go new file mode 100644 index 0000000000000000000000000000000000000000..071015ff28bea5774fda0b0280231ef679fa8051 --- /dev/null +++ b/packages/batchworkerpool/batchworkerpool.go @@ -0,0 +1,163 @@ +package batchworkerpool + +import ( + "sync" + "time" +) + +type BatchWorkerPool struct { + workerFnc func([]Task) + options *Options + + calls chan Task + batchedCalls chan []Task + terminate chan int + + running bool + mutex sync.RWMutex + wait sync.WaitGroup +} + +func New(workerFnc func([]Task), optionalOptions ...Option) (result *BatchWorkerPool) { + options := DEFAULT_OPTIONS.Override(optionalOptions...) + + result = &BatchWorkerPool{ + workerFnc: workerFnc, + options: options, + } + + result.resetChannels() + + return +} + +func (wp *BatchWorkerPool) Submit(params ...interface{}) (result chan interface{}) { + result = make(chan interface{}, 1) + + wp.mutex.RLock() + + if wp.running { + wp.calls <- Task{ + params: params, + resultChan: result, + } + } else { + close(result) + } + + wp.mutex.RUnlock() + + return +} + +func (wp *BatchWorkerPool) Start() { + wp.mutex.Lock() + + if !wp.running { + wp.running = true + + wp.startBatchDispatcher() + wp.startBatchWorkers() + } + + wp.mutex.Unlock() +} + +func (wp *BatchWorkerPool) Run() { + wp.Start() + + wp.wait.Wait() +} + +func (wp *BatchWorkerPool) Stop() { + go wp.StopAndWait() +} + +func (wp *BatchWorkerPool) StopAndWait() { + wp.mutex.Lock() + + if wp.running { + wp.running = false + + close(wp.terminate) + wp.resetChannels() + } + + wp.wait.Wait() + + wp.mutex.Unlock() +} + +func (wp *BatchWorkerPool) resetChannels() { + wp.calls = make(chan Task, wp.options.QueueSize) + wp.batchedCalls = make(chan []Task, 2*wp.options.WorkerCount) + wp.terminate = make(chan int, 1) +} + +func (wp *BatchWorkerPool) startBatchDispatcher() { + calls := wp.calls + terminate := wp.terminate + + wp.wait.Add(1) + + go func() { + for { + select { + case <-terminate: + wp.wait.Done() + + return + case firstCall := <-calls: + batchTask := append(make([]Task, 0), firstCall) + + collectionTimeout := time.After(wp.options.BatchCollectionTimeout) + + // collect additional requests that arrive within the timeout + CollectAdditionalCalls: + for { + select { + case <-terminate: + wp.wait.Done() + + return + case <-collectionTimeout: + break CollectAdditionalCalls + case call := <-wp.calls: + batchTask = append(batchTask, call) + + if len(batchTask) == wp.options.BatchSize { + break CollectAdditionalCalls + } + } + } + + wp.batchedCalls <- batchTask + } + } + }() +} + +func (wp *BatchWorkerPool) startBatchWorkers() { + batchedCalls := wp.batchedCalls + terminate := wp.terminate + + for i := 0; i < wp.options.WorkerCount; i++ { + wp.wait.Add(1) + + go func() { + aborted := false + + for !aborted { + select { + case <-terminate: + aborted = true + + case batchTask := <-batchedCalls: + wp.workerFnc(batchTask) + } + } + + wp.wait.Done() + }() + } +} diff --git a/packages/batchworkerpool/options.go b/packages/batchworkerpool/options.go new file mode 100644 index 0000000000000000000000000000000000000000..47e53b75931c1b7b9c065fd344fac684fb3adccc --- /dev/null +++ b/packages/batchworkerpool/options.go @@ -0,0 +1,55 @@ +package batchworkerpool + +import ( + "runtime" + "time" +) + +var DEFAULT_OPTIONS = &Options{ + WorkerCount: 2 * runtime.NumCPU(), + QueueSize: 2 * runtime.NumCPU() * 64, + BatchSize: 64, + BatchCollectionTimeout: 15 * time.Millisecond, +} + +func WorkerCount(workerCount int) Option { + return func(args *Options) { + args.WorkerCount = workerCount + } +} + +func BatchSize(batchSize int) Option { + return func(args *Options) { + args.BatchSize = batchSize + } +} + +func BatchCollectionTimeout(batchCollectionTimeout time.Duration) Option { + return func(args *Options) { + args.BatchCollectionTimeout = batchCollectionTimeout + } +} + +func QueueSize(queueSize int) Option { + return func(args *Options) { + args.QueueSize = queueSize + } +} + +type Options struct { + WorkerCount int + QueueSize int + BatchSize int + BatchCollectionTimeout time.Duration +} + +func (options Options) Override(optionalOptions ...Option) *Options { + result := &options + for _, option := range optionalOptions { + option(result) + } + + return result +} + +type Option func(*Options) diff --git a/packages/batchworkerpool/task.go b/packages/batchworkerpool/task.go new file mode 100644 index 0000000000000000000000000000000000000000..7d4ea82e80b0ff0e44714bdea19851d6f6ea329f --- /dev/null +++ b/packages/batchworkerpool/task.go @@ -0,0 +1,15 @@ +package batchworkerpool + +type Task struct { + params []interface{} + resultChan chan interface{} +} + +func (task *Task) Return(result interface{}) { + task.resultChan <- result + close(task.resultChan) +} + +func (task *Task) Param(index int) interface{} { + return task.params[index] +} diff --git a/packages/curl/batch_hasher.go b/packages/curl/batch_hasher.go index 367ff8e0c64618265a420fd0c3725d1ad7b12994..88e38b5e891ff29dcb9e28d0789c11c244aab437 100644 --- a/packages/curl/batch_hasher.go +++ b/packages/curl/batch_hasher.go @@ -3,79 +3,39 @@ package curl import ( "fmt" "strconv" - "time" + "github.com/iotaledger/goshimmer/packages/batchworkerpool" "github.com/iotaledger/goshimmer/packages/ternary" ) -type HashRequest struct { - input ternary.Trits - output chan ternary.Trits -} - type BatchHasher struct { - hashRequests chan HashRequest - tasks chan []HashRequest - hashLength int - rounds int + hashLength int + rounds int + workerPool *batchworkerpool.BatchWorkerPool } -func NewBatchHasher(hashLength int, rounds int) *BatchHasher { - this := &BatchHasher{ - hashLength: hashLength, - rounds: rounds, - hashRequests: make(chan HashRequest), - tasks: make(chan []HashRequest, NUMBER_OF_WORKERS), +func NewBatchHasher(hashLength int, rounds int) (result *BatchHasher) { + result = &BatchHasher{ + hashLength: hashLength, + rounds: rounds, } - go this.startDispatcher() - this.startWorkers() + result.workerPool = batchworkerpool.New(result.processHashes, batchworkerpool.BatchSize(strconv.IntSize)) + result.workerPool.Start() - return this + return } -func (this *BatchHasher) startWorkers() { - for i := 0; i < NUMBER_OF_WORKERS; i++ { - go func() { - for { - this.processHashes(<-this.tasks) - } - }() - } +func (this *BatchHasher) Hash(trits ternary.Trits) ternary.Trits { + return (<-this.workerPool.Submit(trits)).(ternary.Trits) } -func (this *BatchHasher) startDispatcher() { - for { - collectedHashRequests := make([]HashRequest, 0) - - // wait for first request to start processing at all - collectedHashRequests = append(collectedHashRequests, <-this.hashRequests) - - // collect additional requests that arrive within the timeout - CollectAdditionalRequests: - for { - select { - case hashRequest := <-this.hashRequests: - collectedHashRequests = append(collectedHashRequests, hashRequest) - - if len(collectedHashRequests) == strconv.IntSize { - break CollectAdditionalRequests - } - case <-time.After(50 * time.Millisecond): - break CollectAdditionalRequests - } - } - - this.tasks <- collectedHashRequests - } -} - -func (this *BatchHasher) processHashes(collectedHashRequests []HashRequest) { - if len(collectedHashRequests) > 1 { +func (this *BatchHasher) processHashes(tasks []batchworkerpool.Task) { + if len(tasks) > 1 { // multiplex the requests multiplexer := ternary.NewBCTernaryMultiplexer() - for _, hashRequest := range collectedHashRequests { - multiplexer.Add(hashRequest.input) + for _, hashRequest := range tasks { + multiplexer.Add(hashRequest.Param(0).(ternary.Trits)) } bcTrits, err := multiplexer.Extract() if err != nil { @@ -89,33 +49,18 @@ func (this *BatchHasher) processHashes(collectedHashRequests []HashRequest) { // extract the results from the demultiplexer demux := ternary.NewBCTernaryDemultiplexer(bctCurl.Squeeze(243)) - for i, hashRequest := range collectedHashRequests { - hashRequest.output <- demux.Get(i) - close(hashRequest.output) + for i, task := range tasks { + task.Return(demux.Get(i)) } } else { var resp = make(ternary.Trits, this.hashLength) + trits := tasks[0].Param(0).(ternary.Trits) + curl := NewCurl(this.hashLength, this.rounds) - curl.Absorb(collectedHashRequests[0].input, 0, len(collectedHashRequests[0].input)) + curl.Absorb(trits, 0, len(trits)) curl.Squeeze(resp, 0, this.hashLength) - collectedHashRequests[0].output <- resp - close(collectedHashRequests[0].output) + tasks[0].Return(resp) } } - -func (this *BatchHasher) Hash(trits ternary.Trits) chan ternary.Trits { - hashRequest := HashRequest{ - input: trits, - output: make(chan ternary.Trits, 1), - } - - this.hashRequests <- hashRequest - - return hashRequest.output -} - -const ( - NUMBER_OF_WORKERS = 1000 -) diff --git a/packages/curl/batch_hasher_test.go b/packages/curl/batch_hasher_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9c9c3eec8f53c77e940f6121dee18291d1eddbef --- /dev/null +++ b/packages/curl/batch_hasher_test.go @@ -0,0 +1,27 @@ +package curl + +import ( + "sync" + "testing" + + "github.com/iotaledger/goshimmer/packages/ternary" +) + +func BenchmarkBatchHasher_Hash(b *testing.B) { + batchHasher := NewBatchHasher(243, 81) + tritsToHash := ternary.Trytes("A999999FF").ToTrits() + + b.ResetTimer() + + var wg sync.WaitGroup + for i := 0; i < b.N; i++ { + wg.Add(1) + + go func() { + batchHasher.Hash(tritsToHash) + + wg.Done() + }() + } + wg.Wait() +} diff --git a/packages/model/meta_transaction/meta_transaction.go b/packages/model/meta_transaction/meta_transaction.go index 58669c8bfe650ca478be302f05678a3c3e99bc39..e4f983cd62a9966ec674fcef7a2c5b63898f55f2 100644 --- a/packages/model/meta_transaction/meta_transaction.go +++ b/packages/model/meta_transaction/meta_transaction.go @@ -115,7 +115,7 @@ func (this *MetaTransaction) GetWeightMagnitude() (result int) { // hashes the transaction using curl (without locking - internal usage) func (this *MetaTransaction) parseHashRelatedDetails() { - hashTrits := <-curl.CURLP81.Hash(this.trits) + hashTrits := curl.CURLP81.Hash(this.trits) hashTrytes := hashTrits.ToTrytes() this.hash = &hashTrytes diff --git a/packages/workerpool/batchworkerpool.go b/packages/workerpool/batchworkerpool.go new file mode 100644 index 0000000000000000000000000000000000000000..bfd2d03762c6643662dca5c01bc32137a05edec4 --- /dev/null +++ b/packages/workerpool/batchworkerpool.go @@ -0,0 +1,95 @@ +package workerpool + +import ( + "runtime" + "time" +) + +type BatchWorkerPoolOptions struct { + WorkerCount int + QueueSize int + MaxBatchSize int + BatchCollectionTimeout time.Duration +} + +type BatchWorkerPool struct { + workerFnc func([]Call) + options BatchWorkerPoolOptions + callsChan chan Call + batchedCallsChan chan []Call +} + +func NewBatchWorkerPool(workerFnc func([]Call), options BatchWorkerPoolOptions) *BatchWorkerPool { + return &BatchWorkerPool{ + workerFnc: workerFnc, + options: options, + + callsChan: make(chan Call, options.QueueSize), + batchedCallsChan: make(chan []Call, 2*options.WorkerCount), + } +} + +func (wp *BatchWorkerPool) Submit(params ...interface{}) (result chan interface{}) { + result = make(chan interface{}, 1) + + wp.callsChan <- Call{ + params: params, + resultChan: result, + } + + return +} + +func (wp *BatchWorkerPool) Start() { + wp.startBatchDispatcher() + wp.startBatchWorkers() +} + +func (wp *BatchWorkerPool) startBatchDispatcher() { + go func() { + for { + // wait for first request to start processing at all + batchTask := append(make([]Call, 0), <-wp.callsChan) + + collectionTimeout := time.After(wp.options.BatchCollectionTimeout) + + // collect additional requests that arrive within the timeout + CollectAdditionalCalls: + for { + select { + case <-collectionTimeout: + break CollectAdditionalCalls + case call := <-wp.callsChan: + batchTask = append(batchTask, call) + + if len(batchTask) == wp.options.MaxBatchSize { + break CollectAdditionalCalls + } + } + } + + wp.batchedCallsChan <- batchTask + } + }() +} + +func (wp *BatchWorkerPool) startBatchWorkers() { + for i := 0; i < wp.options.WorkerCount; i++ { + go func() { + for { + batchTask := <-wp.batchedCallsChan + + wp.workerFnc(batchTask) + } + }() + } +} + +var ( + DEFAULT_OPTIONS = BatchWorkerPoolOptions{ + WorkerCount: 2 * runtime.NumCPU(), + QueueSize: 500, + MaxBatchSize: 64, + BatchCollectionTimeout: 10 * time.Millisecond, + } +) diff --git a/packages/workerpool/batchworkerpool_test.go b/packages/workerpool/batchworkerpool_test.go new file mode 100644 index 0000000000000000000000000000000000000000..737842abf3bfda282b6c340c76a6789fba916f67 --- /dev/null +++ b/packages/workerpool/batchworkerpool_test.go @@ -0,0 +1,90 @@ +package workerpool + +import ( + "fmt" + "sync" + "testing" + + "github.com/iotaledger/goshimmer/packages/curl" + "github.com/iotaledger/goshimmer/packages/ternary" +) + +func Benchmark(b *testing.B) { + trits := ternary.Trytes("A99999999999999999999999A99999999999999999999999A99999999999999999999999A99999999999999999999999").ToTrits() + + options := DEFAULT_OPTIONS + options.QueueSize = 5000 + + pool := NewBatchWorkerPool(processBatchHashRequests, options) + pool.Start() + + b.ResetTimer() + + var wg sync.WaitGroup + for i := 0; i < b.N; i++ { + resultChan := pool.Submit(trits) + + wg.Add(1) + go func() { + <-resultChan + + wg.Done() + }() + } + wg.Wait() +} + +func TestBatchWorkerPool(t *testing.T) { + pool := NewBatchWorkerPool(processBatchHashRequests, DEFAULT_OPTIONS) + pool.Start() + + trits := ternary.Trytes("A99999").ToTrits() + + var wg sync.WaitGroup + for i := 0; i < 10007; i++ { + resultChan := pool.Submit(trits) + + wg.Add(1) + go func() { + <-resultChan + + wg.Done() + }() + } + wg.Wait() +} + +func processBatchHashRequests(calls []Call) { + if len(calls) > 1 { + // multiplex the requests + multiplexer := ternary.NewBCTernaryMultiplexer() + for _, hashRequest := range calls { + multiplexer.Add(hashRequest.params[0].(ternary.Trits)) + } + bcTrits, err := multiplexer.Extract() + if err != nil { + fmt.Println(err) + } + + // calculate the hash + bctCurl := curl.NewBCTCurl(243, 81) + bctCurl.Reset() + bctCurl.Absorb(bcTrits) + + // extract the results from the demultiplexer + demux := ternary.NewBCTernaryDemultiplexer(bctCurl.Squeeze(243)) + for i, hashRequest := range calls { + hashRequest.resultChan <- demux.Get(i) + close(hashRequest.resultChan) + } + } else { + var resp = make(ternary.Trits, 243) + + curl := curl.NewCurl(243, 81) + curl.Absorb(calls[0].params[0].(ternary.Trits), 0, len(calls[0].params[0].(ternary.Trits))) + curl.Squeeze(resp, 0, 81) + + calls[0].resultChan <- resp + close(calls[0].resultChan) + } +} diff --git a/packages/workerpool/workerpool.go b/packages/workerpool/workerpool.go new file mode 100644 index 0000000000000000000000000000000000000000..eba10b1f6f2fe1e4609455004ed1351ad629fb54 --- /dev/null +++ b/packages/workerpool/workerpool.go @@ -0,0 +1,51 @@ +package workerpool + +import ( + "fmt" +) + +type WorkerPool struct { + maxWorkers int + MaxQueueSize int + callsChan chan Call +} + +func New(maxWorkers int) *WorkerPool { + return &WorkerPool{ + maxWorkers: maxWorkers, + } +} + +type Call struct { + params []interface{} + resultChan chan interface{} +} + +func (wp *WorkerPool) Submit(params ...interface{}) (result chan interface{}) { + result = make(chan interface{}, 1) + + wp.callsChan <- Call{ + params: params, + resultChan: result, + } + + return +} + +func (wp *WorkerPool) startWorkers() { + for i := 0; i < wp.maxWorkers; i++ { + go func() { + for { + batchTasks := <-wp.callsChan + + fmt.Println(batchTasks) + } + }() + } +} + +func (wp *WorkerPool) Start() { + wp.callsChan = make(chan Call, 2*wp.maxWorkers) + + wp.startWorkers() +} diff --git a/plugins/bundleprocessor/bundleprocessor.go b/plugins/bundleprocessor/bundleprocessor.go new file mode 100644 index 0000000000000000000000000000000000000000..9e21e1662a86aee2145d92314e399567537c9377 --- /dev/null +++ b/plugins/bundleprocessor/bundleprocessor.go @@ -0,0 +1,81 @@ +package bundleprocessor + +import ( + "github.com/iotaledger/goshimmer/packages/errors" + "github.com/iotaledger/goshimmer/packages/model/bundle" + "github.com/iotaledger/goshimmer/packages/model/transactionmetadata" + "github.com/iotaledger/goshimmer/packages/model/value_transaction" + "github.com/iotaledger/goshimmer/packages/ternary" + "github.com/iotaledger/goshimmer/plugins/tangle" +) + +func ProcessSolidBundleHead(headTransaction *value_transaction.ValueTransaction) (*bundle.Bundle, errors.IdentifiableError) { + // only process the bundle if we didn't process it, yet + return tangle.GetBundle(headTransaction.GetHash(), func(headTransactionHash ternary.Trytes) (*bundle.Bundle, errors.IdentifiableError) { + // abort if bundle syntax is wrong + if !headTransaction.IsHead() { + return nil, ErrProcessBundleFailed.Derive(errors.New("invalid parameter"), "transaction needs to be head of bundle") + } + + // initialize event variables + newBundle := bundle.New(headTransactionHash) + bundleTransactions := make([]*value_transaction.ValueTransaction, 0) + + // iterate through trunk transactions until we reach the tail + currentTransaction := headTransaction + for { + // abort if we reached a previous head + if currentTransaction.IsHead() && currentTransaction != headTransaction { + newBundle.SetTransactionHashes(mapTransactionsToTransactionHashes(bundleTransactions)) + + Events.InvalidBundleReceived.Trigger(newBundle, bundleTransactions) + + return nil, ErrProcessBundleFailed.Derive(errors.New("invalid bundle found"), "missing bundle tail") + } + + // update bundle transactions + bundleTransactions = append(bundleTransactions, currentTransaction) + + // retrieve & update metadata + currentTransactionMetadata, dbErr := tangle.GetTransactionMetadata(currentTransaction.GetHash(), transactionmetadata.New) + if dbErr != nil { + return nil, ErrProcessBundleFailed.Derive(dbErr, "failed to retrieve transaction metadata") + } + currentTransactionMetadata.SetBundleHeadHash(headTransactionHash) + + // update value bundle flag + if !newBundle.IsValueBundle() && currentTransaction.GetValue() != 0 { + newBundle.SetValueBundle(true) + } + + // if we are done -> trigger events + if currentTransaction.IsTail() { + newBundle.SetTransactionHashes(mapTransactionsToTransactionHashes(bundleTransactions)) + + if newBundle.IsValueBundle() { + Events.ValueBundleReceived.Trigger(newBundle, bundleTransactions) + } else { + Events.DataBundleReceived.Trigger(newBundle, bundleTransactions) + } + + return newBundle, nil + } + + // try to iterate to next turn + if nextTransaction, err := tangle.GetTransaction(currentTransaction.GetTrunkTransactionHash()); err != nil { + return nil, ErrProcessBundleFailed.Derive(err, "failed to retrieve trunk while processing bundle") + } else { + currentTransaction = nextTransaction + } + } + }) +} + +func mapTransactionsToTransactionHashes(transactions []*value_transaction.ValueTransaction) (result []ternary.Trytes) { + result = make([]ternary.Trytes, len(transactions)) + for k, v := range transactions { + result[k] = v.GetHash() + } + + return +} diff --git a/plugins/bundleprocessor/plugin.go b/plugins/bundleprocessor/plugin.go index 304772311684bbbdd9da88ad31b897d328d5628a..1a1a55c3ae7227db5e07c5066fdcacccf9b39944 100644 --- a/plugins/bundleprocessor/plugin.go +++ b/plugins/bundleprocessor/plugin.go @@ -1,13 +1,9 @@ package bundleprocessor import ( - "github.com/iotaledger/goshimmer/packages/errors" "github.com/iotaledger/goshimmer/packages/events" - "github.com/iotaledger/goshimmer/packages/model/bundle" - "github.com/iotaledger/goshimmer/packages/model/transactionmetadata" "github.com/iotaledger/goshimmer/packages/model/value_transaction" "github.com/iotaledger/goshimmer/packages/node" - "github.com/iotaledger/goshimmer/packages/ternary" "github.com/iotaledger/goshimmer/plugins/tangle" ) @@ -22,74 +18,3 @@ func configure(plugin *node.Plugin) { } })) } - -func ProcessSolidBundleHead(headTransaction *value_transaction.ValueTransaction) (*bundle.Bundle, errors.IdentifiableError) { - // only process the bundle if we didn't process it, yet - return tangle.GetBundle(headTransaction.GetHash(), func(headTransactionHash ternary.Trytes) (*bundle.Bundle, errors.IdentifiableError) { - // abort if bundle syntax is wrong - if !headTransaction.IsHead() { - return nil, ErrProcessBundleFailed.Derive(errors.New("invalid parameter"), "transaction needs to be head of bundle") - } - - // initialize event variables - newBundle := bundle.New(headTransactionHash) - bundleTransactions := make([]*value_transaction.ValueTransaction, 0) - - // iterate through trunk transactions until we reach the tail - currentTransaction := headTransaction - for { - // abort if we reached a previous head - if currentTransaction.IsHead() && currentTransaction != headTransaction { - newBundle.SetTransactionHashes(mapTransactionsToTransactionHashes(bundleTransactions)) - - Events.InvalidBundleReceived.Trigger(newBundle, bundleTransactions) - - return nil, ErrProcessBundleFailed.Derive(errors.New("invalid bundle found"), "missing bundle tail") - } - - // update bundle transactions - bundleTransactions = append(bundleTransactions, currentTransaction) - - // retrieve & update metadata - currentTransactionMetadata, dbErr := tangle.GetTransactionMetadata(currentTransaction.GetHash(), transactionmetadata.New) - if dbErr != nil { - return nil, ErrProcessBundleFailed.Derive(dbErr, "failed to retrieve transaction metadata") - } - currentTransactionMetadata.SetBundleHeadHash(headTransactionHash) - - // update value bundle flag - if !newBundle.IsValueBundle() && currentTransaction.GetValue() != 0 { - newBundle.SetValueBundle(true) - } - - // if we are done -> trigger events - if currentTransaction.IsTail() { - newBundle.SetTransactionHashes(mapTransactionsToTransactionHashes(bundleTransactions)) - - if newBundle.IsValueBundle() { - Events.ValueBundleReceived.Trigger(newBundle, bundleTransactions) - } else { - Events.DataBundleReceived.Trigger(newBundle, bundleTransactions) - } - - return newBundle, nil - } - - // try to iterate to next turn - if nextTransaction, err := tangle.GetTransaction(currentTransaction.GetTrunkTransactionHash()); err != nil { - return nil, ErrProcessBundleFailed.Derive(err, "failed to retrieve trunk while processing bundle") - } else { - currentTransaction = nextTransaction - } - } - }) -} - -func mapTransactionsToTransactionHashes(transactions []*value_transaction.ValueTransaction) (result []ternary.Trytes) { - result = make([]ternary.Trytes, len(transactions)) - for k, v := range transactions { - result[k] = v.GetHash() - } - - return -}