github.com/murrekatt/go-ethereum@v1.5.8-0.20170123175102-fc52f2c007fb/whisper/whisperv5/whisper.go (about)

     1  // Copyright 2016 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package whisperv5
    18  
    19  import (
    20  	"bytes"
    21  	"crypto/ecdsa"
    22  	crand "crypto/rand"
    23  	"crypto/sha256"
    24  	"fmt"
    25  	"runtime"
    26  	"sync"
    27  	"time"
    28  
    29  	"github.com/ethereum/go-ethereum/common"
    30  	"github.com/ethereum/go-ethereum/crypto"
    31  	"github.com/ethereum/go-ethereum/logger"
    32  	"github.com/ethereum/go-ethereum/logger/glog"
    33  	"github.com/ethereum/go-ethereum/p2p"
    34  	"github.com/ethereum/go-ethereum/rlp"
    35  	"golang.org/x/crypto/pbkdf2"
    36  	set "gopkg.in/fatih/set.v0"
    37  )
    38  
    39  // Whisper represents a dark communication interface through the Ethereum
    40  // network, using its very own P2P communication layer.
    41  type Whisper struct {
    42  	protocol p2p.Protocol
    43  	filters  *Filters
    44  
    45  	privateKeys map[string]*ecdsa.PrivateKey
    46  	symKeys     map[string][]byte
    47  	keyMu       sync.RWMutex
    48  
    49  	envelopes   map[common.Hash]*Envelope        // Pool of envelopes currently tracked by this node
    50  	messages    map[common.Hash]*ReceivedMessage // Pool of successfully decrypted messages, which are not expired yet
    51  	expirations map[uint32]*set.SetNonTS         // Message expiration pool
    52  	poolMu      sync.RWMutex                     // Mutex to sync the message and expiration pools
    53  
    54  	peers  map[*Peer]struct{} // Set of currently active peers
    55  	peerMu sync.RWMutex       // Mutex to sync the active peer set
    56  
    57  	mailServer MailServer
    58  
    59  	messageQueue chan *Envelope
    60  	p2pMsgQueue  chan *Envelope
    61  	quit         chan struct{}
    62  
    63  	overflow bool
    64  	test     bool
    65  }
    66  
    67  // New creates a Whisper client ready to communicate through the Ethereum P2P network.
    68  // Param s should be passed if you want to implement mail server, otherwise nil.
    69  func NewWhisper(server MailServer) *Whisper {
    70  	whisper := &Whisper{
    71  		privateKeys:  make(map[string]*ecdsa.PrivateKey),
    72  		symKeys:      make(map[string][]byte),
    73  		envelopes:    make(map[common.Hash]*Envelope),
    74  		messages:     make(map[common.Hash]*ReceivedMessage),
    75  		expirations:  make(map[uint32]*set.SetNonTS),
    76  		peers:        make(map[*Peer]struct{}),
    77  		mailServer:   server,
    78  		messageQueue: make(chan *Envelope, messageQueueLimit),
    79  		p2pMsgQueue:  make(chan *Envelope, messageQueueLimit),
    80  		quit:         make(chan struct{}),
    81  	}
    82  	whisper.filters = NewFilters(whisper)
    83  
    84  	// p2p whisper sub protocol handler
    85  	whisper.protocol = p2p.Protocol{
    86  		Name:    ProtocolName,
    87  		Version: uint(ProtocolVersion),
    88  		Length:  NumberOfMessageCodes,
    89  		Run:     whisper.HandlePeer,
    90  	}
    91  
    92  	return whisper
    93  }
    94  
    95  // Protocols returns the whisper sub-protocols ran by this particular client.
    96  func (w *Whisper) Protocols() []p2p.Protocol {
    97  	return []p2p.Protocol{w.protocol}
    98  }
    99  
   100  // Version returns the whisper sub-protocols version number.
   101  func (w *Whisper) Version() uint {
   102  	return w.protocol.Version
   103  }
   104  
   105  func (w *Whisper) getPeer(peerID []byte) (*Peer, error) {
   106  	w.peerMu.Lock()
   107  	defer w.peerMu.Unlock()
   108  	for p := range w.peers {
   109  		id := p.peer.ID()
   110  		if bytes.Equal(peerID, id[:]) {
   111  			return p, nil
   112  		}
   113  	}
   114  	return nil, fmt.Errorf("Could not find peer with ID: %x", peerID)
   115  }
   116  
   117  // MarkPeerTrusted marks specific peer trusted, which will allow it
   118  // to send historic (expired) messages.
   119  func (w *Whisper) MarkPeerTrusted(peerID []byte) error {
   120  	p, err := w.getPeer(peerID)
   121  	if err != nil {
   122  		return err
   123  	}
   124  	p.trusted = true
   125  	return nil
   126  }
   127  
   128  func (w *Whisper) RequestHistoricMessages(peerID []byte, data []byte) error {
   129  	p, err := w.getPeer(peerID)
   130  	if err != nil {
   131  		return err
   132  	}
   133  	p.trusted = true
   134  	return p2p.Send(p.ws, p2pRequestCode, data)
   135  }
   136  
   137  func (w *Whisper) SendP2PMessage(peerID []byte, envelope *Envelope) error {
   138  	p, err := w.getPeer(peerID)
   139  	if err != nil {
   140  		return err
   141  	}
   142  	return p2p.Send(p.ws, p2pCode, envelope)
   143  }
   144  
   145  // NewIdentity generates a new cryptographic identity for the client, and injects
   146  // it into the known identities for message decryption.
   147  func (w *Whisper) NewIdentity() *ecdsa.PrivateKey {
   148  	key, err := crypto.GenerateKey()
   149  	if err != nil || !validatePrivateKey(key) {
   150  		key, err = crypto.GenerateKey() // retry once
   151  	}
   152  	if err != nil {
   153  		panic(err)
   154  	}
   155  	if !validatePrivateKey(key) {
   156  		panic("Failed to generate valid key")
   157  	}
   158  	w.keyMu.Lock()
   159  	defer w.keyMu.Unlock()
   160  	w.privateKeys[common.ToHex(crypto.FromECDSAPub(&key.PublicKey))] = key
   161  	return key
   162  }
   163  
   164  // DeleteIdentity deletes the specified key if it exists.
   165  func (w *Whisper) DeleteIdentity(key string) {
   166  	w.keyMu.Lock()
   167  	defer w.keyMu.Unlock()
   168  	delete(w.privateKeys, key)
   169  }
   170  
   171  // HasIdentity checks if the the whisper node is configured with the private key
   172  // of the specified public pair.
   173  func (w *Whisper) HasIdentity(pubKey string) bool {
   174  	w.keyMu.RLock()
   175  	defer w.keyMu.RUnlock()
   176  	return w.privateKeys[pubKey] != nil
   177  }
   178  
   179  // GetIdentity retrieves the private key of the specified public identity.
   180  func (w *Whisper) GetIdentity(pubKey string) *ecdsa.PrivateKey {
   181  	w.keyMu.RLock()
   182  	defer w.keyMu.RUnlock()
   183  	return w.privateKeys[pubKey]
   184  }
   185  
   186  func (w *Whisper) GenerateSymKey(name string) error {
   187  	const size = aesKeyLength * 2
   188  	buf := make([]byte, size)
   189  	_, err := crand.Read(buf)
   190  	if err != nil {
   191  		return err
   192  	} else if !validateSymmetricKey(buf) {
   193  		return fmt.Errorf("error in GenerateSymKey: crypto/rand failed to generate random data")
   194  	}
   195  
   196  	key := buf[:aesKeyLength]
   197  	salt := buf[aesKeyLength:]
   198  	derived, err := DeriveOneTimeKey(key, salt, EnvelopeVersion)
   199  	if err != nil {
   200  		return err
   201  	} else if !validateSymmetricKey(derived) {
   202  		return fmt.Errorf("failed to derive valid key")
   203  	}
   204  
   205  	w.keyMu.Lock()
   206  	defer w.keyMu.Unlock()
   207  
   208  	if w.symKeys[name] != nil {
   209  		return fmt.Errorf("Key with name [%s] already exists", name)
   210  	}
   211  	w.symKeys[name] = derived
   212  	return nil
   213  }
   214  
   215  func (w *Whisper) AddSymKey(name string, key []byte) error {
   216  	if w.HasSymKey(name) {
   217  		return fmt.Errorf("Key with name [%s] already exists", name)
   218  	}
   219  
   220  	derived, err := deriveKeyMaterial(key, EnvelopeVersion)
   221  	if err != nil {
   222  		return err
   223  	}
   224  
   225  	w.keyMu.Lock()
   226  	defer w.keyMu.Unlock()
   227  
   228  	// double check is necessary, because deriveKeyMaterial() is slow
   229  	if w.symKeys[name] != nil {
   230  		return fmt.Errorf("Key with name [%s] already exists", name)
   231  	}
   232  	w.symKeys[name] = derived
   233  	return nil
   234  }
   235  
   236  func (w *Whisper) HasSymKey(name string) bool {
   237  	w.keyMu.RLock()
   238  	defer w.keyMu.RUnlock()
   239  	return w.symKeys[name] != nil
   240  }
   241  
   242  func (w *Whisper) DeleteSymKey(name string) {
   243  	w.keyMu.Lock()
   244  	defer w.keyMu.Unlock()
   245  	delete(w.symKeys, name)
   246  }
   247  
   248  func (w *Whisper) GetSymKey(name string) []byte {
   249  	w.keyMu.RLock()
   250  	defer w.keyMu.RUnlock()
   251  	return w.symKeys[name]
   252  }
   253  
   254  // Watch installs a new message handler to run in case a matching packet arrives
   255  // from the whisper network.
   256  func (w *Whisper) Watch(f *Filter) uint32 {
   257  	return w.filters.Install(f)
   258  }
   259  
   260  func (w *Whisper) GetFilter(id uint32) *Filter {
   261  	return w.filters.Get(id)
   262  }
   263  
   264  // Unwatch removes an installed message handler.
   265  func (w *Whisper) Unwatch(id uint32) {
   266  	w.filters.Uninstall(id)
   267  }
   268  
   269  // Send injects a message into the whisper send queue, to be distributed in the
   270  // network in the coming cycles.
   271  func (w *Whisper) Send(envelope *Envelope) error {
   272  	return w.add(envelope)
   273  }
   274  
   275  // Start implements node.Service, starting the background data propagation thread
   276  // of the Whisper protocol.
   277  func (w *Whisper) Start(*p2p.Server) error {
   278  	glog.V(logger.Info).Infoln("Whisper started")
   279  	go w.update()
   280  
   281  	numCPU := runtime.NumCPU()
   282  	for i := 0; i < numCPU; i++ {
   283  		go w.processQueue()
   284  	}
   285  
   286  	return nil
   287  }
   288  
   289  // Stop implements node.Service, stopping the background data propagation thread
   290  // of the Whisper protocol.
   291  func (w *Whisper) Stop() error {
   292  	close(w.quit)
   293  	glog.V(logger.Info).Infoln("Whisper stopped")
   294  	return nil
   295  }
   296  
   297  // handlePeer is called by the underlying P2P layer when the whisper sub-protocol
   298  // connection is negotiated.
   299  func (wh *Whisper) HandlePeer(peer *p2p.Peer, rw p2p.MsgReadWriter) error {
   300  	// Create the new peer and start tracking it
   301  	whisperPeer := newPeer(wh, peer, rw)
   302  
   303  	wh.peerMu.Lock()
   304  	wh.peers[whisperPeer] = struct{}{}
   305  	wh.peerMu.Unlock()
   306  
   307  	defer func() {
   308  		wh.peerMu.Lock()
   309  		delete(wh.peers, whisperPeer)
   310  		wh.peerMu.Unlock()
   311  	}()
   312  
   313  	// Run the peer handshake and state updates
   314  	if err := whisperPeer.handshake(); err != nil {
   315  		return err
   316  	}
   317  	whisperPeer.start()
   318  	defer whisperPeer.stop()
   319  
   320  	return wh.runMessageLoop(whisperPeer, rw)
   321  }
   322  
   323  // runMessageLoop reads and processes inbound messages directly to merge into client-global state.
   324  func (wh *Whisper) runMessageLoop(p *Peer, rw p2p.MsgReadWriter) error {
   325  	for {
   326  		// fetch the next packet
   327  		packet, err := rw.ReadMsg()
   328  		if err != nil {
   329  			return err
   330  		}
   331  
   332  		switch packet.Code {
   333  		case statusCode:
   334  			// this should not happen, but no need to panic; just ignore this message.
   335  			glog.V(logger.Warn).Infof("%v: unxepected status message received", p.peer)
   336  		case messagesCode:
   337  			// decode the contained envelopes
   338  			var envelopes []*Envelope
   339  			if err := packet.Decode(&envelopes); err != nil {
   340  				glog.V(logger.Warn).Infof("%v: failed to decode envelope: [%v], peer will be disconnected", p.peer, err)
   341  				return fmt.Errorf("garbage received")
   342  			}
   343  			// inject all envelopes into the internal pool
   344  			for _, envelope := range envelopes {
   345  				if err := wh.add(envelope); err != nil {
   346  					glog.V(logger.Warn).Infof("%v: bad envelope received: [%v], peer will be disconnected", p.peer, err)
   347  					return fmt.Errorf("invalid envelope")
   348  				}
   349  				p.mark(envelope)
   350  				if wh.mailServer != nil {
   351  					wh.mailServer.Archive(envelope)
   352  				}
   353  			}
   354  		case p2pCode:
   355  			// peer-to-peer message, sent directly to peer bypassing PoW checks, etc.
   356  			// this message is not supposed to be forwarded to other peers, and
   357  			// therefore might not satisfy the PoW, expiry and other requirements.
   358  			// these messages are only accepted from the trusted peer.
   359  			if p.trusted {
   360  				var envelopes []*Envelope
   361  				if err := packet.Decode(&envelopes); err != nil {
   362  					glog.V(logger.Warn).Infof("%v: failed to decode direct message: [%v], peer will be disconnected", p.peer, err)
   363  					return fmt.Errorf("garbage received (directMessage)")
   364  				}
   365  				for _, envelope := range envelopes {
   366  					wh.postEvent(envelope, true)
   367  				}
   368  			}
   369  		case p2pRequestCode:
   370  			// Must be processed if mail server is implemented. Otherwise ignore.
   371  			if wh.mailServer != nil {
   372  				s := rlp.NewStream(packet.Payload, uint64(packet.Size))
   373  				data, err := s.Bytes()
   374  				if err == nil {
   375  					wh.mailServer.DeliverMail(p, data)
   376  				} else {
   377  					glog.V(logger.Error).Infof("%v: bad requestHistoricMessages received: [%v]", p.peer, err)
   378  				}
   379  			}
   380  		default:
   381  			// New message types might be implemented in the future versions of Whisper.
   382  			// For forward compatibility, just ignore.
   383  		}
   384  
   385  		packet.Discard()
   386  	}
   387  }
   388  
   389  // add inserts a new envelope into the message pool to be distributed within the
   390  // whisper network. It also inserts the envelope into the expiration pool at the
   391  // appropriate time-stamp. In case of error, connection should be dropped.
   392  func (wh *Whisper) add(envelope *Envelope) error {
   393  	now := uint32(time.Now().Unix())
   394  	sent := envelope.Expiry - envelope.TTL
   395  
   396  	if sent > now {
   397  		if sent-SynchAllowance > now {
   398  			return fmt.Errorf("envelope created in the future [%x]", envelope.Hash())
   399  		} else {
   400  			// recalculate PoW, adjusted for the time difference, plus one second for latency
   401  			envelope.calculatePoW(sent - now + 1)
   402  		}
   403  	}
   404  
   405  	if envelope.Expiry < now {
   406  		if envelope.Expiry+SynchAllowance*2 < now {
   407  			return fmt.Errorf("very old message")
   408  		} else {
   409  			glog.V(logger.Debug).Infof("expired envelope dropped [%x]", envelope.Hash())
   410  			return nil // drop envelope without error
   411  		}
   412  	}
   413  
   414  	if len(envelope.Data) > MaxMessageLength {
   415  		return fmt.Errorf("huge messages are not allowed [%x]", envelope.Hash())
   416  	}
   417  
   418  	if len(envelope.Version) > 4 {
   419  		return fmt.Errorf("oversized version [%x]", envelope.Hash())
   420  	}
   421  
   422  	if len(envelope.AESNonce) > AESNonceMaxLength {
   423  		// the standard AES GSM nonce size is 12,
   424  		// but const gcmStandardNonceSize cannot be accessed directly
   425  		return fmt.Errorf("oversized AESNonce [%x]", envelope.Hash())
   426  	}
   427  
   428  	if len(envelope.Salt) > saltLength {
   429  		return fmt.Errorf("oversized salt [%x]", envelope.Hash())
   430  	}
   431  
   432  	if envelope.PoW() < MinimumPoW && !wh.test {
   433  		glog.V(logger.Debug).Infof("envelope with low PoW dropped: %f [%x]", envelope.PoW(), envelope.Hash())
   434  		return nil // drop envelope without error
   435  	}
   436  
   437  	hash := envelope.Hash()
   438  
   439  	wh.poolMu.Lock()
   440  	_, alreadyCached := wh.envelopes[hash]
   441  	if !alreadyCached {
   442  		wh.envelopes[hash] = envelope
   443  		if wh.expirations[envelope.Expiry] == nil {
   444  			wh.expirations[envelope.Expiry] = set.NewNonTS()
   445  		}
   446  		if !wh.expirations[envelope.Expiry].Has(hash) {
   447  			wh.expirations[envelope.Expiry].Add(hash)
   448  		}
   449  	}
   450  	wh.poolMu.Unlock()
   451  
   452  	if alreadyCached {
   453  		glog.V(logger.Detail).Infof("whisper envelope already cached [%x]\n", envelope.Hash())
   454  	} else {
   455  		glog.V(logger.Detail).Infof("cached whisper envelope [%x]: %v\n", envelope.Hash(), envelope)
   456  		wh.postEvent(envelope, false) // notify the local node about the new message
   457  	}
   458  	return nil
   459  }
   460  
   461  // postEvent queues the message for further processing.
   462  func (w *Whisper) postEvent(envelope *Envelope, isP2P bool) {
   463  	// if the version of incoming message is higher than
   464  	// currently supported version, we can not decrypt it,
   465  	// and therefore just ignore this message
   466  	if envelope.Ver() <= EnvelopeVersion {
   467  		if isP2P {
   468  			w.p2pMsgQueue <- envelope
   469  		} else {
   470  			w.checkOverflow()
   471  			w.messageQueue <- envelope
   472  		}
   473  	}
   474  }
   475  
   476  // checkOverflow checks if message queue overflow occurs and reports it if necessary.
   477  func (w *Whisper) checkOverflow() {
   478  	queueSize := len(w.messageQueue)
   479  
   480  	if queueSize == messageQueueLimit {
   481  		if !w.overflow {
   482  			w.overflow = true
   483  			glog.V(logger.Warn).Infoln("message queue overflow")
   484  		}
   485  	} else if queueSize <= messageQueueLimit/2 {
   486  		if w.overflow {
   487  			w.overflow = false
   488  		}
   489  	}
   490  }
   491  
   492  // processQueue delivers the messages to the watchers during the lifetime of the whisper node.
   493  func (w *Whisper) processQueue() {
   494  	var e *Envelope
   495  	for {
   496  		select {
   497  		case <-w.quit:
   498  			return
   499  
   500  		case e = <-w.messageQueue:
   501  			w.filters.NotifyWatchers(e, false)
   502  
   503  		case e = <-w.p2pMsgQueue:
   504  			w.filters.NotifyWatchers(e, true)
   505  		}
   506  	}
   507  }
   508  
   509  // update loops until the lifetime of the whisper node, updating its internal
   510  // state by expiring stale messages from the pool.
   511  func (w *Whisper) update() {
   512  	// Start a ticker to check for expirations
   513  	expire := time.NewTicker(expirationCycle)
   514  
   515  	// Repeat updates until termination is requested
   516  	for {
   517  		select {
   518  		case <-expire.C:
   519  			w.expire()
   520  
   521  		case <-w.quit:
   522  			return
   523  		}
   524  	}
   525  }
   526  
   527  // expire iterates over all the expiration timestamps, removing all stale
   528  // messages from the pools.
   529  func (w *Whisper) expire() {
   530  	w.poolMu.Lock()
   531  	defer w.poolMu.Unlock()
   532  
   533  	now := uint32(time.Now().Unix())
   534  	for then, hashSet := range w.expirations {
   535  		// Short circuit if a future time
   536  		if then > now {
   537  			continue
   538  		}
   539  		// Dump all expired messages and remove timestamp
   540  		hashSet.Each(func(v interface{}) bool {
   541  			delete(w.envelopes, v.(common.Hash))
   542  			delete(w.messages, v.(common.Hash))
   543  			return true
   544  		})
   545  		w.expirations[then].Clear()
   546  	}
   547  }
   548  
   549  // envelopes retrieves all the messages currently pooled by the node.
   550  func (w *Whisper) Envelopes() []*Envelope {
   551  	w.poolMu.RLock()
   552  	defer w.poolMu.RUnlock()
   553  
   554  	all := make([]*Envelope, 0, len(w.envelopes))
   555  	for _, envelope := range w.envelopes {
   556  		all = append(all, envelope)
   557  	}
   558  	return all
   559  }
   560  
   561  // Messages retrieves all the decrypted messages matching a filter id.
   562  func (w *Whisper) Messages(id uint32) []*ReceivedMessage {
   563  	result := make([]*ReceivedMessage, 0)
   564  	w.poolMu.RLock()
   565  	defer w.poolMu.RUnlock()
   566  
   567  	if filter := w.filters.Get(id); filter != nil {
   568  		for _, msg := range w.messages {
   569  			if filter.MatchMessage(msg) {
   570  				result = append(result, msg)
   571  			}
   572  		}
   573  	}
   574  	return result
   575  }
   576  
   577  func (w *Whisper) addDecryptedMessage(msg *ReceivedMessage) {
   578  	w.poolMu.Lock()
   579  	defer w.poolMu.Unlock()
   580  
   581  	w.messages[msg.EnvelopeHash] = msg
   582  }
   583  
   584  func ValidatePublicKey(k *ecdsa.PublicKey) bool {
   585  	return k != nil && k.X != nil && k.Y != nil && k.X.Sign() != 0 && k.Y.Sign() != 0
   586  }
   587  
   588  func validatePrivateKey(k *ecdsa.PrivateKey) bool {
   589  	if k == nil || k.D == nil || k.D.Sign() == 0 {
   590  		return false
   591  	}
   592  	return ValidatePublicKey(&k.PublicKey)
   593  }
   594  
   595  // validateSymmetricKey returns false if the key contains all zeros
   596  func validateSymmetricKey(k []byte) bool {
   597  	return len(k) > 0 && !containsOnlyZeros(k)
   598  }
   599  
   600  func containsOnlyZeros(data []byte) bool {
   601  	for _, b := range data {
   602  		if b != 0 {
   603  			return false
   604  		}
   605  	}
   606  	return true
   607  }
   608  
   609  func bytesToIntLittleEndian(b []byte) (res uint64) {
   610  	mul := uint64(1)
   611  	for i := 0; i < len(b); i++ {
   612  		res += uint64(b[i]) * mul
   613  		mul *= 256
   614  	}
   615  	return res
   616  }
   617  
   618  func BytesToIntBigEndian(b []byte) (res uint64) {
   619  	for i := 0; i < len(b); i++ {
   620  		res *= 256
   621  		res += uint64(b[i])
   622  	}
   623  	return res
   624  }
   625  
   626  // DeriveSymmetricKey derives symmetric key material from the key or password.
   627  // pbkdf2 is used for security, in case people use password instead of randomly generated keys.
   628  func deriveKeyMaterial(key []byte, version uint64) (derivedKey []byte, err error) {
   629  	if version == 0 {
   630  		// kdf should run no less than 0.1 seconds on average compute,
   631  		// because it's a once in a session experience
   632  		derivedKey := pbkdf2.Key(key, nil, 65356, aesKeyLength, sha256.New)
   633  		return derivedKey, nil
   634  	} else {
   635  		return nil, unknownVersionError(version)
   636  	}
   637  }