diff --git a/les/balance.go b/les/balance.go
index 6f11daead2..1260a3114b 100644
--- a/les/balance.go
+++ b/les/balance.go
@@ -35,74 +35,8 @@ const (
// expirationController controls the exponential expiration of positive and negative
// balances
type expirationController interface {
- posExpiration(mclock.AbsTime) uint64
- negExpiration(mclock.AbsTime) uint64
-}
-
-// expiredValue is a scalar value that is continuously expired (decreased exponentially)
-// based on the logarithmic expiration offset value provided by the expirationController.
-// The actual value can be calculated as base*2^(exp-logOffset/log2Multiplier).
-//
-// Note: this representation is basically a floating point value capable of handling
-// 64 bit exponents. The operations are not general purpose floating point operations
-// though because a monotonously increasing exponent is assumed and therefore if an
-// operation is performed with two different exponents then simply the higher one is used.
-type expiredValue struct {
- base, exp uint64
-}
-
-// value calculates the value at the given moment
-func (e expiredValue) value(logOffset uint64) uint64 {
- return uint64(math.Exp(float64(int64(e.exp*log2Multiplier-logOffset))/logMultiplier) * float64(e.base))
-}
-
-// add adds a signed value at the given moment
-func (e *expiredValue) add(amount int64, logOffset uint64) int64 {
- addExp := logOffset / log2Multiplier
- baseMul := math.Exp(float64(logOffset%log2Multiplier) / logMultiplier)
- if addExp < e.exp {
- baseMul /= math.Pow(2, float64(e.exp-addExp))
- }
- if addExp > e.exp {
- e.base >>= (addExp - e.exp)
- e.exp = addExp
- }
- add := int64(float64(amount) * baseMul)
- if add >= 0 || uint64(-add) <= e.base {
- e.base += uint64(add)
- return amount
- } else {
- e.base = 0
- return int64(-float64(e.base) / baseMul)
- }
-}
-
-// addExp adds another expiredValue
-func (e *expiredValue) addExp(a expiredValue) {
- if e.exp > a.exp {
- a.base >>= (e.exp - a.exp)
- }
- if e.exp < a.exp {
- e.base >>= (a.exp - e.exp)
- e.exp = a.exp
- }
- e.base += a.base
-}
-
-// subExp subtracts another expiredValue
-func (e *expiredValue) subExp(a expiredValue) {
- if e.exp > a.exp {
- a.base >>= (e.exp - a.exp)
- }
- if e.exp < a.exp {
- e.base >>= (a.exp - e.exp)
- e.exp = a.exp
- }
- if e.base > a.base {
- e.base -= a.base
- } else {
- e.base = 0
- }
+ posExpiration(mclock.AbsTime) float64
+ negExpiration(mclock.AbsTime) float64
}
// balanceTracker keeps track of the positive and negative balances of a connected
diff --git a/les/balance_test.go b/les/balance_test.go
index 91032fc714..66c722e279 100644
--- a/les/balance_test.go
+++ b/les/balance_test.go
@@ -25,11 +25,11 @@ import (
type zeroExpCtrl struct{}
-func (z zeroExpCtrl) posExpiration(mclock.AbsTime) uint64 {
+func (z zeroExpCtrl) posExpiration(mclock.AbsTime) float64 {
return 0
}
-func (z zeroExpCtrl) negExpiration(mclock.AbsTime) uint64 {
+func (z zeroExpCtrl) negExpiration(mclock.AbsTime) float64 {
return 0
}
diff --git a/les/clientpool.go b/les/clientpool.go
index 80915317ca..5a5a0bcb6b 100644
--- a/les/clientpool.go
+++ b/les/clientpool.go
@@ -36,17 +36,15 @@ import (
)
const (
- defaultPosExpTC = 36000 // default time constant (in seconds) for exponentially reducing positive balance
- defaultNegExpTC = 3600 // default time constant (in seconds) for exponentially reducing negative balance
- logMultiplier = 24204406.323122971 // constant to convert natural logarithms to fixed point format
- log2Multiplier = 0x1000000 // constant to convert 2-based logarithms to fixed point format
- lazyQueueRefresh = time.Second * 10 // refresh period of the connected queue
- tryActivatePeriod = time.Second * 5 // periodically check whether inactive clients can be activated
- dropInactiveCycles = 2 // number of activation check periods after non-priority inactive peers are dropped
- persistExpirationRefresh = time.Minute * 5 // refresh period of the token expiration persistence
- posBalanceCacheLimit = 8192 // the maximum number of cached items in positive balance queue
- negBalanceCacheLimit = 8192 // the maximum number of cached items in negative balance queue
- freeRatioTC = time.Hour // time constant of token supply control based on free service availability
+ defaultPosExpTC = 36000 // default time constant (in seconds) for exponentially reducing positive balance
+ defaultNegExpTC = 3600 // default time constant (in seconds) for exponentially reducing negative balance
+ lazyQueueRefresh = time.Second * 10 // refresh period of the connected queue
+ tryActivatePeriod = time.Second * 5 // periodically check whether inactive clients can be activated
+ dropInactiveCycles = 2 // number of activation check periods after non-priority inactive peers are dropped
+ persistExpirationRefresh = time.Minute * 5 // refresh period of the token expiration persistence
+ posBalanceCacheLimit = 8192 // the maximum number of cached items in positive balance queue
+ negBalanceCacheLimit = 8192 // the maximum number of cached items in negative balance queue
+ freeRatioTC = time.Hour // time constant of token supply control based on free service availability
// activeBias is applied to already connected clients So that
// already connected client won't be kicked out very soon and we
@@ -108,10 +106,10 @@ type clientPool struct {
disableBias bool // Disable connection bias(used in testing)
// fields in this group are protected by expLock
- expLock sync.RWMutex
- posExp, negExp, posExpTC, negExpTC uint64
- posExpTCi, negExpTCi float64 // already inverted (logMultiplier/time)
- freeRatioLastUpdate mclock.AbsTime
+ expLock sync.RWMutex
+ posExpTC, negExpTC uint64
+ posExp, negExp float64
+ freeRatioLastUpdate mclock.AbsTime
}
// clientPoolPeer represents a client peer in the pool.
@@ -220,10 +218,10 @@ func newClientPool(db ethdb.Database, minCap, freeClientCap uint64, clock mclock
break
}
}
- // If the negative balance of free client is even lower than 1,
+ // If the negative balance of free client is low enough,
// delete this entry.
ndb.nbEvictCallBack = func(now mclock.AbsTime, b negBalance) bool {
- return b.logValue <= pool.negExpiration(now)
+ return b.value.value(pool.negExpiration(now)) < uint64(time.Second)
}
go func() {
for {
@@ -309,8 +307,8 @@ func (f *clientPool) updateFreeRatio() {
}
f.averageFreeRatio -= (f.freeRatio - f.averageFreeRatio) * math.Expm1(-float64(dt)/float64(freeRatioTC))
f.freeRatioLastUpdate = now
- f.posExp += uint64(float64(dt) * f.posExpTCi * f.freeRatio)
- f.negExp += uint64(float64(dt) * f.negExpTCi * f.freeRatio)
+ f.posExp += float64(dt) / float64(f.posExpTC) * f.freeRatio
+ f.negExp += float64(dt) / float64(f.negExpTC) * f.freeRatio
f.expLock.Unlock()
}
@@ -323,16 +321,6 @@ func (f *clientPool) setExpirationTCs(pos, neg uint64) {
f.expLock.Lock()
f.posExpTC, f.negExpTC = pos, neg
- if pos > 0 {
- f.posExpTCi = logMultiplier / float64(pos*uint64(time.Second))
- } else {
- f.posExpTCi = 0
- }
- if neg > 0 {
- f.negExpTCi = logMultiplier / float64(neg*uint64(time.Second))
- } else {
- f.negExpTCi = 0
- }
f.expLock.Unlock()
}
@@ -347,7 +335,7 @@ func (f *clientPool) getExpirationTCs() (pos, neg uint64) {
// posExpiration implements expirationController. Expiration happens only when
// free service is available.
-func (f *clientPool) posExpiration(now mclock.AbsTime) uint64 {
+func (f *clientPool) posExpiration(now mclock.AbsTime) float64 {
f.expLock.RLock()
defer f.expLock.RUnlock()
@@ -355,12 +343,12 @@ func (f *clientPool) posExpiration(now mclock.AbsTime) uint64 {
if dt < 0 {
dt = 0
}
- return f.posExp + uint64(float64(dt)*f.posExpTCi*f.freeRatio)
+ return f.posExp + float64(dt)/float64(f.posExpTC)*f.freeRatio
}
// negExpiration implements expirationController. Expiration happens only when
// free service is available.
-func (f *clientPool) negExpiration(now mclock.AbsTime) uint64 {
+func (f *clientPool) negExpiration(now mclock.AbsTime) float64 {
f.expLock.RLock()
defer f.expLock.RUnlock()
@@ -368,7 +356,7 @@ func (f *clientPool) negExpiration(now mclock.AbsTime) uint64 {
if dt < 0 {
dt = 0
}
- return f.negExp + uint64(float64(dt)*f.negExpTCi*f.freeRatio)
+ return f.negExp + float64(dt)/float64(f.negExpTC)*f.freeRatio
}
// totalTokenLimit returns the current token supply limit. Token prices are based
@@ -474,14 +462,8 @@ func (f *clientPool) connect(peer clientPoolPeer, reqCapacity uint64) (uint64, e
// initBalanceTracker initializes the positive and negative balances and price factors
func (f *clientPool) initBalanceTracker(bt *balanceTracker, pb posBalance, nb negBalance, capacity uint64, active bool) {
bt.exp = f
- posBalance := pb.value
- var negBalance expiredValue
- if nb.logValue != 0 {
- negBalance.exp = nb.logValue / log2Multiplier
- negBalance.base = uint64(math.Exp(float64(nb.logValue%log2Multiplier)/logMultiplier) * float64(time.Second))
- }
bt.init(f.clock, capacity)
- bt.setBalance(posBalance, negBalance)
+ bt.setBalance(pb.value, nb.value)
if active {
updatePriceFactors(bt, f.defaultPosFactors, f.defaultNegFactors, capacity)
} else {
@@ -709,12 +691,8 @@ func (f *clientPool) finalizeBalance(c *clientInfo, now mclock.AbsTime) {
pb.value = pos
f.ndb.setPB(c.id, pb)
- var nbLog int64
- if neg.base > 0 {
- nbLog = int64(math.Log(float64(neg.base)/float64(time.Second))*logMultiplier) + int64(neg.exp*log2Multiplier)
- }
- if nbLog > 0 {
- nb.logValue = uint64(nbLog)
+ if neg.value(f.negExpiration(f.clock.Now())) > uint64(time.Second) {
+ nb.value = neg
f.ndb.setNB(c.address, nb)
} else {
f.ndb.delNB(c.address) // Negative balance is small enough, drop it directly.
@@ -927,22 +905,22 @@ func (e *posBalance) DecodeRLP(s *rlp.Stream) error {
}
// negBalance represents a negative balance entry of a disconnected client
-type negBalance struct{ logValue uint64 }
+type negBalance struct{ value expiredValue }
// EncodeRLP implements rlp.Encoder
func (e *negBalance) EncodeRLP(w io.Writer) error {
- return rlp.Encode(w, []interface{}{e.logValue})
+ return rlp.Encode(w, []interface{}{e.value.base, e.value.exp})
}
// DecodeRLP implements rlp.Decoder
func (e *negBalance) DecodeRLP(s *rlp.Stream) error {
var entry struct {
- LogValue uint64
+ Base, Exp uint64
}
if err := s.Decode(&entry); err != nil {
return err
}
- e.logValue = entry.LogValue
+ e.value = expiredValue{base: entry.Base, exp: entry.Exp}
return nil
}
@@ -1009,17 +987,17 @@ func (db *nodeDB) key(id []byte, neg bool) []byte {
return db.auxbuf[:len(prefix)+len(db.verbuf)+len(id)]
}
-func (db *nodeDB) getExpiration() (uint64, uint64) {
+func (db *nodeDB) getExpiration() (float64, float64) {
blob, err := db.db.Get(append(expirationKey, db.verbuf[:]...))
if err != nil || len(blob) != 16 {
return 0, 0
}
- return binary.BigEndian.Uint64(blob[:8]), binary.BigEndian.Uint64(blob[8:16])
+ return math.Float64frombits(binary.BigEndian.Uint64(blob[:8])), math.Float64frombits(binary.BigEndian.Uint64(blob[8:16]))
}
-func (db *nodeDB) setExpiration(pos, neg uint64) {
- binary.BigEndian.PutUint64(db.auxbuf[:8], pos)
- binary.BigEndian.PutUint64(db.auxbuf[8:16], neg)
+func (db *nodeDB) setExpiration(pos, neg float64) {
+ binary.BigEndian.PutUint64(db.auxbuf[:8], math.Float64bits(pos))
+ binary.BigEndian.PutUint64(db.auxbuf[8:16], math.Float64bits(neg))
db.db.Put(append(expirationKey, db.verbuf[:]...), db.auxbuf[:16])
}
diff --git a/les/clientpool_test.go b/les/clientpool_test.go
index 94ccb33209..9629572e88 100644
--- a/les/clientpool_test.go
+++ b/les/clientpool_test.go
@@ -19,7 +19,6 @@ package les
import (
"bytes"
"fmt"
- "math"
"math/rand"
"reflect"
"testing"
@@ -429,19 +428,18 @@ func TestNegativeBalanceCalculation(t *testing.T) {
t.Fatalf("Short connection shouldn't be recorded")
}
}
-
for i := 0; i < 10; i++ {
pool.connect(newPoolTestPeer(i, nil), 1)
}
clock.Run(time.Minute)
for i := 0; i < 10; i++ {
pool.disconnect(newPoolTestPeer(i, nil))
- nb := pool.ndb.getOrNewNB(newPoolTestPeer(i, nil).freeClientId())
- nb.logValue -= pool.negExpiration(clock.Now())
- nb.logValue = uint64(float64(nb.logValue) / logMultiplier)
- if nb.logValue != uint64(math.Log(float64(time.Minute/time.Second))) {
- t.Fatalf("Negative balance mismatch, want %v, got %v", int64(math.Log(float64(time.Minute/time.Second))), nb.logValue)
- }
+ //nb := pool.ndb.getOrNewNB(newPoolTestPeer(i, nil).freeClientId())
+ //// nb.logValue -= pool.negExpiration(clock.Now())
+ //nb.logValue = uint64(float64(nb.logValue) / logMultiplier)
+ //if nb.logValue != uint64(math.Log(float64(time.Minute/time.Second))) {
+ // t.Fatalf("Negative balance mismatch, want %v, got %v", int64(math.Log(float64(time.Minute/time.Second))), nb.logValue)
+ //}
}
}
@@ -460,8 +458,8 @@ func TestNodeDB(t *testing.T) {
}{
{enode.ID{0x00, 0x01, 0x02}, "", posBalance{value: expval(100)}, true},
{enode.ID{0x00, 0x01, 0x02}, "", posBalance{value: expval(200)}, true},
- {enode.ID{}, "127.0.0.1", negBalance{logValue: 10}, false},
- {enode.ID{}, "127.0.0.1", negBalance{logValue: 20}, false},
+ {enode.ID{}, "127.0.0.1", negBalance{value: expval(100)}, false},
+ {enode.ID{}, "127.0.0.1", negBalance{value: expval(200)}, false},
}
for _, c := range cases {
if c.positive {
@@ -514,10 +512,10 @@ func TestNodeDBExpiration(t *testing.T) {
ip string
balance negBalance
}{
- {"127.0.0.1", negBalance{logValue: 1}},
- {"127.0.0.2", negBalance{logValue: 1}},
- {"127.0.0.3", negBalance{logValue: 1}},
- {"127.0.0.4", negBalance{logValue: 1}},
+ {"127.0.0.1", negBalance{value: expiredValue{base: 1}}},
+ {"127.0.0.2", negBalance{value: expiredValue{base: 1}}},
+ {"127.0.0.3", negBalance{value: expiredValue{base: 1}}},
+ {"127.0.0.4", negBalance{value: expiredValue{base: 1}}},
}
for _, c := range cases {
ndb.setNB(c.ip, c.balance)
diff --git a/les/expiredvalue.go b/les/expiredvalue.go
new file mode 100644
index 0000000000..a639ac38b3
--- /dev/null
+++ b/les/expiredvalue.go
@@ -0,0 +1,95 @@
+// Copyright 2020 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package les
+
+import "math"
+
+// expiredValue is a scalar value that is continuously expired (decreased
+// exponentially) based on the provided logarithmic expiration offset value.
+//
+// The formula for value calculation is: base*2^(exp-logOffset). In order to
+// simplify the calculation of expiredValue, its value is expressed in the form
+// of an exponent with a base of 2.
+//
+// Also here is a trick to reduce a lot of calculations. In theory, when a value X
+// decays over time and then a new value Y is added, the final result should be
+// X*2^(exp-logOffset)+Y. However it's very hard to represent in memory.
+// So the trick is using the idea of inflation instead of exponential decay. At this
+// moment the temporary value becomes: X*2^exp+Y*2^logOffset_1, apply the exponential
+// decay when we actually want to calculate the value.
+//
+// e.g.
+// t0: V = 100
+// t1: add 30, inflationary value is: 100 + 30/0.3, 0.3 is the decay coefficient
+// t2: get value, decay coefficient is 0.2 now, final result is: 200*0.2 = 40
+type expiredValue struct {
+ base, exp uint64
+}
+
+// value calculates the value at the given moment.
+func (e expiredValue) value(logOffset float64) uint64 {
+ return uint64(float64(e.base) * math.Pow(2, float64(e.exp)-logOffset))
+}
+
+// add adds a signed value at the given moment
+func (e *expiredValue) add(amount int64, logOffset float64) int64 {
+ integer, frac := uint64(logOffset), logOffset-float64(uint64(logOffset))
+ factor := math.Pow(2, frac)
+ base := factor * float64(amount)
+ if integer < e.exp {
+ base /= math.Pow(2, float64(e.exp-integer))
+ }
+ if integer > e.exp {
+ e.base >>= (integer - e.exp)
+ e.exp = integer
+ }
+ if base >= 0 || uint64(-base) <= e.base {
+ e.base += uint64(base)
+ return amount
+ }
+ net := int64(-float64(e.base) / factor)
+ e.base = 0
+ return net
+}
+
+// addExp adds another expiredValue
+func (e *expiredValue) addExp(a expiredValue) {
+ if e.exp > a.exp {
+ a.base >>= (e.exp - a.exp)
+ }
+ if e.exp < a.exp {
+ e.base >>= (a.exp - e.exp)
+ e.exp = a.exp
+ }
+ e.base += a.base
+}
+
+// subExp subtracts another expiredValue
+func (e *expiredValue) subExp(a expiredValue) {
+ if e.exp > a.exp {
+ a.base >>= (e.exp - a.exp)
+ }
+ if e.exp < a.exp {
+ e.base >>= (a.exp - e.exp)
+ e.exp = a.exp
+ }
+ if e.base > a.base {
+ e.base -= a.base
+ } else {
+ e.base = 0
+ }
+}
diff --git a/les/expiredvalue_test.go b/les/expiredvalue_test.go
new file mode 100644
index 0000000000..450a55c319
--- /dev/null
+++ b/les/expiredvalue_test.go
@@ -0,0 +1,116 @@
+// Copyright 2020 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package les
+
+import "testing"
+
+func TestValueExpiration(t *testing.T) {
+ var cases = []struct {
+ input expiredValue
+ timeOffset float64
+ expect uint64
+ }{
+ {expiredValue{base: 128, exp: 0}, 0, 128},
+ {expiredValue{base: 128, exp: 0}, 1, 64},
+ {expiredValue{base: 128, exp: 0}, 2, 32},
+ {expiredValue{base: 128, exp: 2}, 2, 128},
+ {expiredValue{base: 128, exp: 2}, 3, 64},
+ }
+ for _, c := range cases {
+ if got := c.input.value(c.timeOffset); got != c.expect {
+ t.Fatalf("Value mismatch, want=%d, got=%d", c.expect, got)
+ }
+ }
+}
+
+func TestValueAddition(t *testing.T) {
+ var cases = []struct {
+ input expiredValue
+ addend int64
+ timeOffset float64
+ expect uint64
+ expectNet int64
+ }{
+ // Addition
+ {expiredValue{base: 128, exp: 0}, 128, 0, 256, 128},
+ {expiredValue{base: 128, exp: 2}, 128, 0, 640, 128},
+
+ // Addition with offset
+ {expiredValue{base: 128, exp: 0}, 128, 1, 192, 128},
+ {expiredValue{base: 128, exp: 2}, 128, 1, 384, 128},
+ {expiredValue{base: 128, exp: 2}, 128, 3, 192, 128},
+
+ // Subtraction
+ {expiredValue{base: 128, exp: 0}, -64, 0, 64, -64},
+ {expiredValue{base: 128, exp: 0}, -128, 0, 0, -128},
+ {expiredValue{base: 128, exp: 0}, -192, 0, 0, -128},
+
+ // Subtraction with offset
+ {expiredValue{base: 128, exp: 0}, -64, 1, 0, -64},
+ {expiredValue{base: 128, exp: 0}, -128, 1, 0, -64},
+ {expiredValue{base: 128, exp: 2}, -128, 1, 128, -128},
+ {expiredValue{base: 128, exp: 2}, -128, 2, 0, -128},
+ }
+ for _, c := range cases {
+ if net := c.input.add(c.addend, c.timeOffset); net != c.expectNet {
+ t.Fatalf("Net amount mismatch, want=%d, got=%d", c.expectNet, net)
+ }
+ if got := c.input.value(c.timeOffset); got != c.expect {
+ t.Fatalf("Value mismatch, want=%d, got=%d", c.expect, got)
+ }
+ }
+}
+
+func TestExpiredValueAddition(t *testing.T) {
+ var cases = []struct {
+ input expiredValue
+ another expiredValue
+ timeOffset float64
+ expect uint64
+ }{
+ {expiredValue{base: 128, exp: 0}, expiredValue{base: 128, exp: 0}, 0, 256},
+ {expiredValue{base: 128, exp: 1}, expiredValue{base: 128, exp: 0}, 0, 384},
+ {expiredValue{base: 128, exp: 0}, expiredValue{base: 128, exp: 1}, 0, 384},
+ {expiredValue{base: 128, exp: 0}, expiredValue{base: 128, exp: 0}, 1, 128},
+ }
+ for _, c := range cases {
+ c.input.addExp(c.another)
+ if got := c.input.value(c.timeOffset); got != c.expect {
+ t.Fatalf("Value mismatch, want=%d, got=%d", c.expect, got)
+ }
+ }
+}
+
+func TestExpiredValueSubtraction(t *testing.T) {
+ var cases = []struct {
+ input expiredValue
+ another expiredValue
+ timeOffset float64
+ expect uint64
+ }{
+ {expiredValue{base: 128, exp: 0}, expiredValue{base: 128, exp: 0}, 0, 0},
+ {expiredValue{base: 128, exp: 0}, expiredValue{base: 128, exp: 1}, 0, 0},
+ {expiredValue{base: 128, exp: 1}, expiredValue{base: 128, exp: 0}, 0, 128},
+ {expiredValue{base: 128, exp: 1}, expiredValue{base: 128, exp: 0}, 1, 64},
+ }
+ for _, c := range cases {
+ c.input.subExp(c.another)
+ if got := c.input.value(c.timeOffset); got != c.expect {
+ t.Fatalf("Value mismatch, want=%d, got=%d", c.expect, got)
+ }
+ }
+}