diff options
author | Jimmy Hu <jimmy.hu@dexon.org> | 2019-05-02 21:07:49 +0800 |
---|---|---|
committer | Jimmy Hu <jimmy.hu@dexon.org> | 2019-05-02 21:35:46 +0800 |
commit | c361083f3f04c2a4e9b50bd2f8fb963e2985ab1d (patch) | |
tree | 40b129b57bc7cd3de8659238550d369afe64a5ad | |
parent | 528b0252cdf359b5c3bd0dd22c96641c06eb5659 (diff) | |
download | dexon-testnet.tar.gz dexon-testnet.tar.zst dexon-testnet.zip |
vendor: sync to latest coretestnet
9 files changed, 169 insertions, 98 deletions
diff --git a/vendor/github.com/dexon-foundation/dexon-consensus/core/agreement-mgr.go b/vendor/github.com/dexon-foundation/dexon-consensus/core/agreement-mgr.go index af0adf259..ab4840acc 100644 --- a/vendor/github.com/dexon-foundation/dexon-consensus/core/agreement-mgr.go +++ b/vendor/github.com/dexon-foundation/dexon-consensus/core/agreement-mgr.go @@ -24,6 +24,8 @@ import ( "sync" "time" + lru "github.com/hashicorp/golang-lru" + "github.com/dexon-foundation/dexon-consensus/common" "github.com/dexon-foundation/dexon-consensus/core/types" typesDKG "github.com/dexon-foundation/dexon-consensus/core/types/dkg" @@ -39,6 +41,7 @@ var ( ) const maxResultCache = 100 +const settingLimit = 3 // genValidLeader generate a validLeader function for agreement modules. func genValidLeader( @@ -114,12 +117,15 @@ type agreementMgr struct { recv *consensusBAReceiver processedBAResult map[types.Position]struct{} voteFilter *utils.VoteFilter + settingCache *lru.Cache + curRoundSetting *baRoundSetting waitGroup sync.WaitGroup isRunning bool lock sync.RWMutex } func newAgreementMgr(con *Consensus) (mgr *agreementMgr, err error) { + settingCache, _ := lru.New(settingLimit) mgr = &agreementMgr{ con: con, ID: con.ID, @@ -133,6 +139,7 @@ func newAgreementMgr(con *Consensus) (mgr *agreementMgr, err error) { ctx: con.ctx, processedBAResult: make(map[types.Position]struct{}, maxResultCache), voteFilter: utils.NewVoteFilter(), + settingCache: settingCache, } mgr.recv = &consensusBAReceiver{ consensus: con, @@ -149,21 +156,18 @@ func (mgr *agreementMgr) prepare() { newLeaderSelector(genValidLeader(mgr), mgr.logger), mgr.signer, mgr.logger) - nodes, err := mgr.cache.GetNodeSet(round) - if err != nil { + setting := mgr.generateSetting(round) + if setting == nil { + mgr.logger.Warn("Unable to prepare init setting", "round", round) return } - agr.notarySet = nodes.GetSubSet( - int(mgr.config(round).notarySetSize), - types.NewNotarySetTarget(mgr.config(round).crs)) + mgr.curRoundSetting = setting + agr.notarySet = mgr.curRoundSetting.dkgSet // Hacky way to make agreement module self contained. mgr.recv.agreementModule = agr mgr.baModule = agr if round >= DKGDelayRound { - setting := mgr.generateSetting(round) - if setting == nil { - mgr.logger.Warn("Unable to prepare init setting", "round", round) - } else if _, exist := setting.dkgSet[mgr.ID]; exist { + if _, exist := setting.dkgSet[mgr.ID]; exist { mgr.logger.Debug("Preparing signer and npks.", "round", round) npk, signer, err := mgr.con.cfgModule.getDKGInfo(round, false) if err != nil { @@ -251,10 +255,34 @@ func (mgr *agreementMgr) notifyRoundEvents(evts []utils.RoundEventParam) error { return nil } +func (mgr *agreementMgr) checkProposer( + round uint64, proposerID types.NodeID) error { + if round == mgr.curRoundSetting.round { + if _, exist := mgr.curRoundSetting.dkgSet[proposerID]; !exist { + return ErrNotInNotarySet + } + } else if round == mgr.curRoundSetting.round+1 { + setting := mgr.generateSetting(round) + if setting == nil { + return ErrConfigurationNotReady + } + if _, exist := setting.dkgSet[proposerID]; !exist { + return ErrNotInNotarySet + } + } + return nil +} + func (mgr *agreementMgr) processVote(v *types.Vote) (err error) { + if !mgr.recv.isNotary { + return nil + } if mgr.voteFilter.Filter(v) { return nil } + if err := mgr.checkProposer(v.Position.Round, v.ProposerID); err != nil { + return err + } if err = mgr.baModule.processVote(v); err == nil { mgr.baModule.updateFilter(mgr.voteFilter) mgr.voteFilter.AddVote(v) @@ -263,6 +291,9 @@ func (mgr *agreementMgr) processVote(v *types.Vote) (err error) { } func (mgr *agreementMgr) processBlock(b *types.Block) error { + if err := mgr.checkProposer(b.Position.Round, b.ProposerID); err != nil { + return err + } return mgr.baModule.processBlock(b) } @@ -323,6 +354,7 @@ func (mgr *agreementMgr) processAgreementResult( result.Position.Round) return ErrConfigurationNotReady } + mgr.curRoundSetting = setting leader, err := mgr.calcLeader(setting.dkgSet, setting.crs, result.Position) if err != nil { return err @@ -358,6 +390,9 @@ func (mgr *agreementMgr) stop() { } func (mgr *agreementMgr) generateSetting(round uint64) *baRoundSetting { + if setting, exist := mgr.settingCache.Get(round); exist { + return setting.(*baRoundSetting) + } curConfig := mgr.config(round) if curConfig == nil { return nil @@ -383,13 +418,15 @@ func (mgr *agreementMgr) generateSetting(round uint64) *baRoundSetting { return nil } } - return &baRoundSetting{ + setting := &baRoundSetting{ crs: curConfig.crs, dkgSet: dkgSet, round: round, threshold: utils.GetBAThreshold(&types.Config{ NotarySetSize: curConfig.notarySetSize}), } + mgr.settingCache.Add(round, setting) + return setting } func (mgr *agreementMgr) runBA(initRound uint64) { @@ -449,6 +486,7 @@ Loop: } mgr.recv.isNotary = checkRound() mgr.voteFilter = utils.NewVoteFilter() + mgr.voteFilter.Position.Round = currentRound mgr.recv.emptyBlockHashMap = &sync.Map{} if currentRound >= DKGDelayRound && mgr.recv.isNotary { var err error @@ -521,11 +559,13 @@ func (mgr *agreementMgr) baRoutineForOneRound( default: } nextHeight, nextTime = mgr.bcModule.nextBlock() - if isStop(restartPos) { - break - } - if nextHeight > restartPos.Height { - break + if nextHeight != notReadyHeight { + if isStop(restartPos) { + break + } + if nextHeight > restartPos.Height { + break + } } mgr.logger.Debug("BlockChain not ready!!!", "old", oldPos, "restart", restartPos, "next", nextHeight) diff --git a/vendor/github.com/dexon-foundation/dexon-consensus/core/agreement.go b/vendor/github.com/dexon-foundation/dexon-consensus/core/agreement.go index b122a4ddf..a2cac11f4 100644 --- a/vendor/github.com/dexon-foundation/dexon-consensus/core/agreement.go +++ b/vendor/github.com/dexon-foundation/dexon-consensus/core/agreement.go @@ -361,9 +361,6 @@ func (a *agreement) sanityCheck(vote *types.Vote) error { if vote.Type >= types.MaxVoteType { return ErrInvalidVote } - if _, exist := a.notarySet[vote.ProposerID]; !exist { - return ErrNotInNotarySet - } ok, err := utils.VerifyVoteSignature(vote) if err != nil { return err @@ -371,6 +368,10 @@ func (a *agreement) sanityCheck(vote *types.Vote) error { if !ok { return ErrIncorrectVoteSignature } + if vote.Position.Round != a.agreementID().Round { + // TODO(jimmy): maybe we can verify partial signature at agreement-mgr. + return nil + } if !a.data.recv.VerifyPartialSignature(vote) { return ErrIncorrectVotePartialSignature } @@ -412,7 +413,7 @@ func (a *agreement) updateFilter(filter *utils.VoteFilter) { filter.Confirm = a.hasOutput filter.LockIter = a.data.lockIter filter.Period = a.data.period - filter.Height = a.agreementID().Height + filter.Position.Height = a.agreementID().Height } // processVote is the entry point for processing Vote. @@ -426,14 +427,15 @@ func (a *agreement) processVote(vote *types.Vote) error { // Agreement module has stopped. if isStop(aID) { - // Hacky way to not drop first votes for genesis height. - if vote.Position.Height == types.GenesisHeight { + // Hacky way to not drop first votes when round just begins. + if vote.Position.Round == aID.Round { a.pendingVote = append(a.pendingVote, pendingVote{ vote: vote, receivedTime: time.Now().UTC(), }) + return nil } - return nil + return ErrSkipButNoError } if vote.Position != aID { if aID.Newer(vote.Position) { @@ -636,19 +638,34 @@ func (a *agreement) confirmedNoLock() bool { // processBlock is the entry point for processing Block. func (a *agreement) processBlock(block *types.Block) error { + checkSkip := func() bool { + aID := a.agreementID() + if block.Position != aID { + // Agreement module has stopped. + if !isStop(aID) { + if aID.Newer(block.Position) { + return true + } + } + } + return false + } + if checkSkip() { + return nil + } + if err := utils.VerifyBlockSignature(block); err != nil { + return err + } + a.lock.Lock() defer a.lock.Unlock() a.data.blocksLock.Lock() defer a.data.blocksLock.Unlock() - aID := a.agreementID() - if block.Position != aID { - // Agreement module has stopped. - if !isStop(aID) { - if aID.Newer(block.Position) { - return nil - } - } + // a.agreementID might change during lock, so we need to checkSkip again. + if checkSkip() { + return nil + } else if aID != block.Position { a.pendingBlock = append(a.pendingBlock, pendingBlock{ block: block, receivedTime: time.Now().UTC(), diff --git a/vendor/github.com/dexon-foundation/dexon-consensus/core/blockchain.go b/vendor/github.com/dexon-foundation/dexon-consensus/core/blockchain.go index 9fbb86162..335e75cca 100644 --- a/vendor/github.com/dexon-foundation/dexon-consensus/core/blockchain.go +++ b/vendor/github.com/dexon-foundation/dexon-consensus/core/blockchain.go @@ -21,6 +21,7 @@ import ( "bytes" "errors" "fmt" + "math" "sort" "sync" "time" @@ -49,6 +50,8 @@ var ( ErrMissingRandomness = errors.New("missing block randomness") ) +const notReadyHeight uint64 = math.MaxUint64 + type pendingBlockRecord struct { position types.Position block *types.Block @@ -387,6 +390,10 @@ func (bc *blockChain) nextBlock() (uint64, time.Time) { if tip == nil { return types.GenesisHeight, bc.dMoment } + if tip != bc.lastDelivered { + // If tip is not delivered, we should not proceed to next block. + return notReadyHeight, time.Time{} + } return tip.Position.Height + 1, tip.Timestamp.Add(config.minBlockInterval) } diff --git a/vendor/github.com/dexon-foundation/dexon-consensus/core/consensus.go b/vendor/github.com/dexon-foundation/dexon-consensus/core/consensus.go index 966c70aaa..b5baace86 100644 --- a/vendor/github.com/dexon-foundation/dexon-consensus/core/consensus.go +++ b/vendor/github.com/dexon-foundation/dexon-consensus/core/consensus.go @@ -93,13 +93,23 @@ func (recv *consensusBAReceiver) VerifyPartialSignature(vote *types.Vote) bool { if vote.Position.Round >= DKGDelayRound && vote.BlockHash != types.SkipBlockHash { if vote.Type == types.VoteCom || vote.Type == types.VoteFastCom { if recv.npks == nil { + recv.consensus.logger.Debug( + "Unable to verify psig, npks is nil", + "vote", vote) return false } if vote.Position.Round != recv.npks.Round { + recv.consensus.logger.Debug( + "Unable to verify psig, round of npks mismatch", + "vote", vote, + "npksRound", recv.npks.Round) return false } pubKey, exist := recv.npks.PublicKeys[vote.ProposerID] if !exist { + recv.consensus.logger.Debug( + "Unable to verify psig, proposer is not qualified", + "vote", vote) return false } blockHash := vote.BlockHash @@ -1114,6 +1124,10 @@ func (con *Consensus) generateBlockRandomness(blocks []*types.Block) { "block", block, "result", result) con.network.BroadcastAgreementResult(result) + if err := con.deliverFinalizedBlocks(); err != nil { + con.logger.Error("Failed to deliver finalized block", + "error", err) + } } }(block) } @@ -1354,11 +1368,7 @@ func (con *Consensus) ProcessAgreementResult( return nil } // Sanity Check. - notarySet, err := con.nodeSetCache.GetNotarySet(rand.Position.Round) - if err != nil { - return err - } - if err := VerifyAgreementResult(rand, notarySet); err != nil { + if err := VerifyAgreementResult(rand, con.nodeSetCache); err != nil { con.baMgr.untouchAgreementResult(rand) return err } diff --git a/vendor/github.com/dexon-foundation/dexon-consensus/core/dkg-tsig-protocol.go b/vendor/github.com/dexon-foundation/dexon-consensus/core/dkg-tsig-protocol.go index 0612bda4b..d81d485c3 100644 --- a/vendor/github.com/dexon-foundation/dexon-consensus/core/dkg-tsig-protocol.go +++ b/vendor/github.com/dexon-foundation/dexon-consensus/core/dkg-tsig-protocol.go @@ -487,10 +487,13 @@ func (d *dkgProtocol) processPrivateShare( if _, exist := d.antiComplaintReceived[prvShare.ReceiverID]; !exist { d.antiComplaintReceived[prvShare.ReceiverID] = make(map[types.NodeID]struct{}) + } + if _, exist := + d.antiComplaintReceived[prvShare.ReceiverID][prvShare.ProposerID]; !exist { d.recv.ProposeDKGAntiNackComplaint(prvShare) + d.antiComplaintReceived[prvShare.ReceiverID][prvShare.ProposerID] = + struct{}{} } - d.antiComplaintReceived[prvShare.ReceiverID][prvShare.ProposerID] = - struct{}{} } return nil } diff --git a/vendor/github.com/dexon-foundation/dexon-consensus/core/syncer/agreement.go b/vendor/github.com/dexon-foundation/dexon-consensus/core/syncer/agreement.go index b414e1146..d39c24627 100644 --- a/vendor/github.com/dexon-foundation/dexon-consensus/core/syncer/agreement.go +++ b/vendor/github.com/dexon-foundation/dexon-consensus/core/syncer/agreement.go @@ -176,12 +176,7 @@ func (a *agreement) processAgreementResult(r *types.AgreementResult) { a.logger.Trace("Agreement result cached", "result", r) return } - notarySet, err := a.cache.GetNotarySet(r.Position.Round) - if err != nil { - a.logger.Error("unable to get notary set", "result", r, "error", err) - return - } - if err := core.VerifyAgreementResult(r, notarySet); err != nil { + if err := core.VerifyAgreementResult(r, a.cache); err != nil { a.logger.Error("Agreement result verification failed", "result", r, "error", err) @@ -257,18 +252,13 @@ func (a *agreement) processNewCRS(round uint64) { a.latestCRSRound = round // Verify all pending results. for r := prevRound; r <= a.latestCRSRound; r++ { - notarySet, err := a.cache.GetNotarySet(r) - if err != nil { - a.logger.Error("Unable to get notary set", "round", r, "error", err) - continue - } pendingsForRound := a.pendingAgrs[r] if pendingsForRound == nil { continue } delete(a.pendingAgrs, r) for _, res := range pendingsForRound { - if err := core.VerifyAgreementResult(res, notarySet); err != nil { + if err := core.VerifyAgreementResult(res, a.cache); err != nil { a.logger.Error("Invalid agreement result", "result", res, "error", err) diff --git a/vendor/github.com/dexon-foundation/dexon-consensus/core/utils.go b/vendor/github.com/dexon-foundation/dexon-consensus/core/utils.go index c9d5f840f..c4d7b0fc3 100644 --- a/vendor/github.com/dexon-foundation/dexon-consensus/core/utils.go +++ b/vendor/github.com/dexon-foundation/dexon-consensus/core/utils.go @@ -158,13 +158,17 @@ func HashConfigurationBlock( // VerifyAgreementResult perform sanity check against a types.AgreementResult // instance. func VerifyAgreementResult( - res *types.AgreementResult, notarySet map[types.NodeID]struct{}) error { + res *types.AgreementResult, cache *NodeSetCache) error { if res.Position.Round >= DKGDelayRound { if len(res.Randomness) == 0 { return ErrMissingRandomness } return nil } + notarySet, err := cache.GetNotarySet(res.Position.Round) + if err != nil { + return err + } if len(res.Votes) < len(notarySet)*2/3+1 { return ErrNotEnoughVotes } diff --git a/vendor/github.com/dexon-foundation/dexon-consensus/core/utils/vote-filter.go b/vendor/github.com/dexon-foundation/dexon-consensus/core/utils/vote-filter.go index 2fc18bb34..446d88a64 100644 --- a/vendor/github.com/dexon-foundation/dexon-consensus/core/utils/vote-filter.go +++ b/vendor/github.com/dexon-foundation/dexon-consensus/core/utils/vote-filter.go @@ -25,7 +25,7 @@ import ( // To maximize performance, this structure is not thread-safe and will never be. type VoteFilter struct { Voted map[types.VoteHeader]struct{} - Height uint64 + Position types.Position LockIter uint64 Period uint64 Confirm bool @@ -43,9 +43,9 @@ func (vf *VoteFilter) Filter(vote *types.Vote) bool { if vote.Type == types.VoteInit { return true } - if vote.Position.Height < vf.Height { + if vote.Position.Older(vf.Position) { return true - } else if vote.Position.Height > vf.Height { + } else if vote.Position.Newer(vf.Position) { // It's impossible to check the vote of other height. return false } diff --git a/vendor/vendor.json b/vendor/vendor.json index 85bda6aa5..6c6c9f6a8 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -141,90 +141,90 @@ { "checksumSHA1": "In6vBHYUsX7DUIGiFN2hQggBgvI=", "path": "github.com/dexon-foundation/dexon-consensus/common", - "revision": "b9492bc09f8bb53b2a36e505ddad0d26373e71a3", - "revisionTime": "2019-04-10T01:52:20Z", - "version": "single-chain", - "versionExact": "single-chain" + "revision": "1edd55a2ff91bbbe94b8c1e8564ea2e5a45cf37c", + "revisionTime": "2019-05-02T12:35:18Z", + "version": "testnet", + "versionExact": "testnet" }, { - "checksumSHA1": "lY1KAGWRuwd7OpzAz5yN7IysMvU=", + "checksumSHA1": "cpz6BH7Hn3SWM/vdKqLmn5T2mZc=", "path": "github.com/dexon-foundation/dexon-consensus/core", - "revision": "b9492bc09f8bb53b2a36e505ddad0d26373e71a3", - "revisionTime": "2019-04-10T01:52:20Z", - "version": "single-chain", - "versionExact": "single-chain" + "revision": "1edd55a2ff91bbbe94b8c1e8564ea2e5a45cf37c", + "revisionTime": "2019-05-02T12:35:18Z", + "version": "testnet", + "versionExact": "testnet" }, { "checksumSHA1": "v4fKR7uhoyufi6hAVO44cFEb+tY=", "path": "github.com/dexon-foundation/dexon-consensus/core/blockdb", "revision": "56e872f84131348adbc0861afb3554bba4a8e5db", "revisionTime": "2018-12-05T06:29:54Z", - "version": "single-chain", - "versionExact": "single-chain" + "version": "testnet", + "versionExact": "testnet" }, { "checksumSHA1": "tQSbYCu5P00lUhKsx3IbBZCuSLY=", "path": "github.com/dexon-foundation/dexon-consensus/core/crypto", - "revision": "b9492bc09f8bb53b2a36e505ddad0d26373e71a3", - "revisionTime": "2019-04-10T01:52:20Z", - "version": "single-chain", - "versionExact": "single-chain" + "revision": "1edd55a2ff91bbbe94b8c1e8564ea2e5a45cf37c", + "revisionTime": "2019-05-02T12:35:18Z", + "version": "testnet", + "versionExact": "testnet" }, { "checksumSHA1": "m5lUT04qSHKtFukvxjnFX5Jo2hI=", "path": "github.com/dexon-foundation/dexon-consensus/core/crypto/dkg", - "revision": "b9492bc09f8bb53b2a36e505ddad0d26373e71a3", - "revisionTime": "2019-04-10T01:52:20Z", - "version": "single-chain", - "versionExact": "single-chain" + "revision": "1edd55a2ff91bbbe94b8c1e8564ea2e5a45cf37c", + "revisionTime": "2019-05-02T12:35:18Z", + "version": "testnet", + "versionExact": "testnet" }, { "checksumSHA1": "BhLKK8RveoLaeXc9UyUKMwQqchU=", "path": "github.com/dexon-foundation/dexon-consensus/core/crypto/ecdsa", - "revision": "b9492bc09f8bb53b2a36e505ddad0d26373e71a3", - "revisionTime": "2019-04-10T01:52:20Z", - "version": "single-chain", - "versionExact": "single-chain" + "revision": "1edd55a2ff91bbbe94b8c1e8564ea2e5a45cf37c", + "revisionTime": "2019-05-02T12:35:18Z", + "version": "testnet", + "versionExact": "testnet" }, { "checksumSHA1": "hj/KetWUHp+1CX+50V0QnCthfWc=", "path": "github.com/dexon-foundation/dexon-consensus/core/db", - "revision": "b9492bc09f8bb53b2a36e505ddad0d26373e71a3", - "revisionTime": "2019-04-10T01:52:20Z", - "version": "single-chain", - "versionExact": "single-chain" + "revision": "1edd55a2ff91bbbe94b8c1e8564ea2e5a45cf37c", + "revisionTime": "2019-05-02T12:35:18Z", + "version": "testnet", + "versionExact": "testnet" }, { - "checksumSHA1": "/OcEQKdtWDyRZibazIsAxJWHUyg=", + "checksumSHA1": "eMoqaQX9T9oO67QM2KHsVVulohA=", "path": "github.com/dexon-foundation/dexon-consensus/core/syncer", - "revision": "b9492bc09f8bb53b2a36e505ddad0d26373e71a3", - "revisionTime": "2019-04-10T01:52:20Z", - "version": "single-chain", - "versionExact": "single-chain" + "revision": "1edd55a2ff91bbbe94b8c1e8564ea2e5a45cf37c", + "revisionTime": "2019-05-02T12:35:18Z", + "version": "testnet", + "versionExact": "testnet" }, { "checksumSHA1": "zIgCdN4FJiAuPGMhB+/9YGK/Wgk=", "path": "github.com/dexon-foundation/dexon-consensus/core/types", - "revision": "b9492bc09f8bb53b2a36e505ddad0d26373e71a3", - "revisionTime": "2019-04-10T01:52:20Z", - "version": "single-chain", - "versionExact": "single-chain" + "revision": "1edd55a2ff91bbbe94b8c1e8564ea2e5a45cf37c", + "revisionTime": "2019-05-02T12:35:18Z", + "version": "testnet", + "versionExact": "testnet" }, { "checksumSHA1": "lbG7yqVgzo2CV/CQPYjG78xp5jg=", "path": "github.com/dexon-foundation/dexon-consensus/core/types/dkg", - "revision": "b9492bc09f8bb53b2a36e505ddad0d26373e71a3", - "revisionTime": "2019-04-10T01:52:20Z", - "version": "single-chain", - "versionExact": "single-chain" + "revision": "1edd55a2ff91bbbe94b8c1e8564ea2e5a45cf37c", + "revisionTime": "2019-05-02T12:35:18Z", + "version": "testnet", + "versionExact": "testnet" }, { - "checksumSHA1": "1VsJIshz0loXnGwCtrMM8SuIo6Y=", + "checksumSHA1": "9O5eHd40xQTizw3wt9JQfUtxw80=", "path": "github.com/dexon-foundation/dexon-consensus/core/utils", - "revision": "b9492bc09f8bb53b2a36e505ddad0d26373e71a3", - "revisionTime": "2019-04-10T01:52:20Z", - "version": "single-chain", - "versionExact": "single-chain" + "revision": "1edd55a2ff91bbbe94b8c1e8564ea2e5a45cf37c", + "revisionTime": "2019-05-02T12:35:18Z", + "version": "testnet", + "versionExact": "testnet" }, { "checksumSHA1": "TAkwduKZqLyimyTPPWIllZWYFuE=", |