Sequences add pointer to high-level data. Proper mapping of sequences to information requests.

This commit is contained in:
Kleissner
2021-04-21 03:27:03 +02:00
parent e432696ad1
commit ec809029d8
8 changed files with 120 additions and 89 deletions

View File

@@ -204,13 +204,25 @@ func contactArbitraryPeer(publicKey *btcec.PublicKey, addresses []*net.UDPAddr)
}
for _, address := range addresses {
sendAllNetworks(publicKey, &PacketRaw{Command: CommandAnnouncement, Payload: packets[0].raw}, address)
sendAllNetworks(publicKey, &PacketRaw{Command: CommandAnnouncement, Payload: packets[0].raw}, address, &bootstrapFindSelf{})
}
}
// cmdResponseFindSelf processes FIND_SELF responses
func (peer *PeerInfo) cmdResponseFindSelf(msg *MessageResponse, Closest []PeerRecord) {
for _, closePeer := range Closest {
// bootstrapFindSelf is a dummy structure assigned to sequences when sending the Announcement message.
// When receiving the Response message, it will know that it was a legitimate bootstrap request.
type bootstrapFindSelf struct {
}
// bootstrapAcceptContacts is the maximum count of contacts considered. It limits the impact of fake peers.
const bootstrapAcceptContacts = 5
// cmdResponseBootstrapFindSelf processes FIND_SELF responses
func (peer *PeerInfo) cmdResponseBootstrapFindSelf(msg *MessageResponse, closest []PeerRecord) {
if len(closest) > bootstrapAcceptContacts {
closest = closest[:bootstrapAcceptContacts]
}
for _, closePeer := range closest {
// Initiate contact. Once a response comes back, the peer is actually added to the list.
contactArbitraryPeer(closePeer.PublicKey, []*net.UDPAddr{{IP: closePeer.IP, Port: int(closePeer.Port)}})
}

View File

@@ -123,52 +123,64 @@ func (peer *PeerInfo) cmdResponse(msg *MessageResponse) {
fmt.Printf("Incoming initial response from %s\n", msg.connection.Address.String())
}
// Each result is checked against the list of information requests. Only 1 response by peer is permitted per query currently.
// Response packets could be duplicated, which could happen due to auto broadcast when the remote peer responds and has multiple connections to self but none was active.
for _, hash := range msg.HashesNotFound {
info := nodesDHT.IRLookup(peer.NodeID, hash)
if info == nil {
continue
// The sequence data is used to correlate this response with the announcement.
if msg.sequence == nil || msg.sequence.data == nil {
// If there is no sequence data but there were results returned, it means we received unsolicited response data. It will be rejected.
if len(msg.HashesNotFound) > 0 || len(msg.Hash2Peers) > 0 || len(msg.FilesEmbed) > 0 {
log.Printf("cmdResponse unsolicited response data received from %s\n", msg.connection.Address.String())
}
info.ActiveNodesSub(1)
return
}
for _, hash2Peer := range msg.Hash2Peers {
info := nodesDHT.IRLookup(peer.NodeID, hash2Peer.ID.Hash)
if info == nil {
// Response to FIND_SELF?
if bytes.Equal(hash2Peer.ID.Hash, nodeID) && len(hash2Peer.Closest) > 0 {
peer.cmdResponseFindSelf(msg, hash2Peer.Closest)
// bootstrap FIND_SELF?
if _, ok := msg.sequence.data.(*bootstrapFindSelf); ok {
for _, hash2Peer := range msg.Hash2Peers {
// Make sure no garbage is returned. The key must be self and only Closest is expected.
if !bytes.Equal(hash2Peer.ID.Hash, nodeID) || len(hash2Peer.Closest) == 0 {
log.Printf("Incoming response to bootstrap FIND_SELF contains invalid data from %s\n", msg.connection.Address.String())
return
}
continue
peer.cmdResponseBootstrapFindSelf(msg, hash2Peer.Closest)
}
info.ResultChan <- &dht.NodeMessage{SenderID: peer.NodeID, Closest: records2Nodes(hash2Peer.Closest, msg.connection.Network), Storing: records2Nodes(hash2Peer.Storing, msg.connection.Network)}
// Future: a terminate field inform wether the remote peer is done sending. For now terminate after 1 packet.
info.ActiveNodesSub(1)
return
}
for _, file := range msg.FilesEmbed {
info := nodesDHT.IRLookup(peer.NodeID, file.ID.Hash)
if info == nil {
continue
// Response to an information request?
if _, ok := msg.sequence.data.(*dht.InformationRequest); ok {
// Future: Once multiple information requests are pooled (multiplexed) into one or multiple Announcement sequences (messages), the responses need to be de-pooled.
// A simple multiplex structure linked via the sequence containing a map (hash 2 IR) could simplify this.
info := msg.sequence.data.(*dht.InformationRequest)
if len(msg.HashesNotFound) > 0 {
info.Done()
}
info.ResultChan <- &dht.NodeMessage{SenderID: peer.NodeID, Data: file.Data}
for _, hash2Peer := range msg.Hash2Peers {
info.ResultChan <- &dht.NodeMessage{SenderID: peer.NodeID, Closest: records2Nodes(hash2Peer.Closest, msg.connection.Network), Storing: records2Nodes(hash2Peer.Storing, msg.connection.Network)}
info.ActiveNodesSub(1)
info.Terminate() // file was found, terminate the request.
if hash2Peer.IsLast {
info.Done()
}
}
for _, file := range msg.FilesEmbed {
info.ResultChan <- &dht.NodeMessage{SenderID: peer.NodeID, Data: file.Data}
info.Done()
info.Terminate() // file was found, terminate the request.
}
}
}
// cmdPing handles an incoming ping message
func (peer *PeerInfo) cmdPing(msg *MessageRaw) {
if peer == nil {
// Unexpected incoming ping, reply with announce message
// Unexpected incoming ping, reply with announcement message. For security reasons the remote peer is not asked for FIND_SELF.
peer, _ = PeerlistAdd(msg.SenderPublicKey, msg.connection)
peer.sendAnnouncement(true, true, nil, nil, nil)
peer.sendAnnouncement(true, false, nil, nil, nil, nil)
}
peer.send(&PacketRaw{Command: CommandPong, Sequence: msg.Sequence})
//fmt.Printf("Incoming ping from %s on %s\n", msg.connection.Address.String(), msg.connection.Address.String())
@@ -201,7 +213,7 @@ func (peer *PeerInfo) cmdLocalDiscovery(msg *MessageAnnouncement) {
// fmt.Printf("Incoming secondary local discovery from %s\n", msg.connection.Address.String())
}
peer.sendAnnouncement(true, true, nil, nil, nil)
peer.sendAnnouncement(true, true, nil, nil, nil, &bootstrapFindSelf{})
}
// pingTime is the time in seconds to send out ping messages

View File

@@ -303,7 +303,7 @@ func (peer *PeerInfo) sendConnection(packet *PacketRaw, connection *Connection)
}
// sendAllNetworks sends a raw packet via all networks. It assigns a new sequence for each sent packet.
func sendAllNetworks(receiverPublicKey *btcec.PublicKey, packet *PacketRaw, remote *net.UDPAddr) (err error) {
func sendAllNetworks(receiverPublicKey *btcec.PublicKey, packet *PacketRaw, remote *net.UDPAddr, sequenceData interface{}) (err error) {
var raw []byte
packet.Protocol = ProtocolVersion
@@ -319,7 +319,7 @@ func sendAllNetworks(receiverPublicKey *btcec.PublicKey, packet *PacketRaw, remo
continue
}
packet.Sequence = msgArbitrarySequence(receiverPublicKey)
packet.Sequence = msgArbitrarySequence(receiverPublicKey, sequenceData).sequence
if raw, err = PacketEncrypt(peerPrivateKey, receiverPublicKey, packet); err != nil {
return err
}
@@ -336,7 +336,7 @@ func sendAllNetworks(receiverPublicKey *btcec.PublicKey, packet *PacketRaw, remo
continue
}
packet.Sequence = msgArbitrarySequence(receiverPublicKey)
packet.Sequence = msgArbitrarySequence(receiverPublicKey, sequenceData).sequence
if raw, err = PacketEncrypt(peerPrivateKey, receiverPublicKey, packet); err != nil {
return err
}

View File

@@ -49,9 +49,9 @@ func initKademlia() {
func (peer *PeerInfo) sendAnnouncementFindNode(request *dht.InformationRequest) {
// If the key is self, send it as FIND_SELF
if bytes.Equal(request.Key, nodeID) {
peer.sendAnnouncement(false, true, nil, nil, nil)
peer.sendAnnouncement(false, true, nil, nil, nil, request)
} else {
peer.sendAnnouncement(false, false, []KeyHash{{Hash: request.Key}}, nil, nil)
peer.sendAnnouncement(false, false, []KeyHash{{Hash: request.Key}}, nil, nil, request)
}
}
@@ -63,11 +63,11 @@ func (peer *PeerInfo) sendAnnouncementFindValue(request *dht.InformationRequest)
findValue = append(findValue, KeyHash{Hash: request.Key})
peer.sendAnnouncement(false, findSelf, findPeer, findValue, nil)
peer.sendAnnouncement(false, findSelf, findPeer, findValue, nil, request)
}
func (peer *PeerInfo) sendAnnouncementStore(fileHash []byte, fileSize uint64) {
peer.sendAnnouncement(false, false, nil, nil, []InfoStore{{ID: KeyHash{Hash: fileHash}, Size: fileSize, Type: 0}})
peer.sendAnnouncement(false, false, nil, nil, []InfoStore{{ID: KeyHash{Hash: fileHash}, Size: fileSize, Type: 0}}, nil)
}
// ---- CORE DATA FUNCTIONS ----
@@ -78,9 +78,9 @@ func Data2Hash(data []byte) (hash []byte) {
}
// GetData returns the requested data. It checks first the local store and then tries via DHT.
func GetData(hash []byte) (data []byte, found bool) {
func GetData(hash []byte) (data []byte, senderNodeID []byte, found bool) {
if data, found = GetDataLocal(hash); found {
return data, found
return data, nodeID, found
}
return GetDataDHT(hash)
@@ -92,9 +92,9 @@ func GetDataLocal(hash []byte) (data []byte, found bool) {
}
// GetDataDHT requests data via DHT
func GetDataDHT(hash []byte) (data []byte, found bool) {
data, found, _ = nodesDHT.Get(hash)
return data, found
func GetDataDHT(hash []byte) (data []byte, senderNodeID []byte, found bool) {
data, senderNodeID, found, _ = nodesDHT.Get(hash)
return data, senderNodeID, found
}
// StoreDataLocal stores data into the local warehouse.

View File

@@ -67,6 +67,7 @@ type MessageRaw struct {
PacketRaw
SenderPublicKey *btcec.PublicKey // Sender Public Key, ECDSA (secp256k1) 257-bit
connection *Connection // Connection that received the packet
sequence *sequenceExpiry // Sequence
}
// MessageAnnouncement is the decoded announcement message.
@@ -455,10 +456,10 @@ func selfFeatureSupport() (feature byte) {
// announcementPacket contains information about a single announcement message
type announcementPacket struct {
raw []byte // The raw packet
hashes [][]byte // List of hashes that are being searched for
sequence uint32 // Sequence number once sent
err error // Sending error, if any
raw []byte // The raw packet
hashes [][]byte // List of hashes that are being searched for
sequence *sequenceExpiry // Sequence
err error // Sending error, if any
}
// msgEncodeAnnouncement encodes an announcement message. It may return multiple messages if the input does not fit into one.
@@ -761,7 +762,7 @@ createPacketLoop:
// pingConnection sends a ping to the target peer via the specified connection
func (peer *PeerInfo) pingConnection(connection *Connection) {
err := peer.sendConnection(&PacketRaw{Command: CommandPing, Sequence: peer.msgNewSequence()}, connection)
err := peer.sendConnection(&PacketRaw{Command: CommandPing, Sequence: peer.msgNewSequence(nil).sequence}, connection)
connection.LastPingOut = time.Now()
if (connection.Status == ConnectionActive || connection.Status == ConnectionRedundant) && IsNetworkErrorFatal(err) {
@@ -774,13 +775,13 @@ func (peer *PeerInfo) Chat(text string) {
peer.send(&PacketRaw{Command: CommandChat, Payload: []byte(text)})
}
// sendAnnouncement sends the announcement message
func (peer *PeerInfo) sendAnnouncement(sendUA, findSelf bool, findPeer []KeyHash, findValue []KeyHash, files []InfoStore) (packets []*announcementPacket) {
// sendAnnouncement sends the announcement message. It acquires a new sequence for each message.
func (peer *PeerInfo) sendAnnouncement(sendUA, findSelf bool, findPeer []KeyHash, findValue []KeyHash, files []InfoStore, sequenceData interface{}) (packets []*announcementPacket) {
packets = msgEncodeAnnouncement(sendUA, findSelf, findPeer, findValue, files)
for _, packet := range packets {
packet.sequence = peer.msgNewSequence()
packet.err = peer.send(&PacketRaw{Command: CommandAnnouncement, Payload: packet.raw, Sequence: packet.sequence})
packet.sequence = peer.msgNewSequence(sequenceData)
packet.err = peer.send(&PacketRaw{Command: CommandAnnouncement, Payload: packet.raw, Sequence: packet.sequence.sequence})
}
return

View File

@@ -30,9 +30,11 @@ var sequences map[string]*sequenceExpiry
var sequencesMutex sync.Mutex
type sequenceExpiry struct {
created time.Time // When the sequence was created.
expires time.Time // When the sequence expires. This can be extended on the fly!
counter int // How many replies used the sequence. Multiple Response messages may be returned for a single Announcement one.
sequence uint32 // Sequence number
created time.Time // When the sequence was created.
expires time.Time // When the sequence expires. This can be extended on the fly!
counter int // How many replies used the sequence. Multiple Response messages may be returned for a single Announcement one.
data interface{} // Optional high-level data associated with the sequence
}
func initMessageSequence() {
@@ -57,40 +59,42 @@ func initMessageSequence() {
// msgNewSequence returns a new sequence and registers is
// Use only for Announcement and Ping messages.
func (peer *PeerInfo) msgNewSequence() (sequence uint32) {
sequence = atomic.AddUint32(&peer.messageSequence, 1)
key := string(peer.PublicKey.SerializeCompressed()) + strconv.FormatUint(uint64(sequence), 10)
func (peer *PeerInfo) msgNewSequence(data interface{}) (info *sequenceExpiry) {
info = &sequenceExpiry{
sequence: atomic.AddUint32(&peer.messageSequence, 1),
created: time.Now(),
expires: time.Now().Add(time.Duration(ReplyTimeout) * time.Second),
data: data,
}
// Add the sequence to the list. Sequences are unique enough that collisions are unlikely and negligible.
key := string(peer.PublicKey.SerializeCompressed()) + strconv.FormatUint(uint64(info.sequence), 10)
sequencesMutex.Lock()
sequences[key] = &sequenceExpiry{
created: time.Now(),
expires: time.Now().Add(time.Duration(ReplyTimeout) * time.Second),
}
sequences[key] = info
sequencesMutex.Unlock()
return sequence
return
}
// msgArbitrarySequence returns an arbitrary sequence to be used for uncontacted peers
func msgArbitrarySequence(publicKey *btcec.PublicKey) (sequence uint32) {
sequence = rand.Uint32()
key := string(publicKey.SerializeCompressed()) + strconv.FormatUint(uint64(sequence), 10)
func msgArbitrarySequence(publicKey *btcec.PublicKey, data interface{}) (info *sequenceExpiry) {
info = &sequenceExpiry{
sequence: rand.Uint32(),
created: time.Now(),
expires: time.Now().Add(time.Duration(ReplyTimeout) * time.Second),
data: data,
}
// Add the sequence to the list. Sequences are unique enough that collisions are unlikely and negligible.
key := string(publicKey.SerializeCompressed()) + strconv.FormatUint(uint64(info.sequence), 10)
sequencesMutex.Lock()
sequences[key] = &sequenceExpiry{
created: time.Now(),
expires: time.Now().Add(time.Duration(ReplyTimeout) * time.Second),
}
sequences[key] = info
sequencesMutex.Unlock()
return sequence
return
}
// msgValidateSequence validates the sequence number of an incoming message
// msgValidateSequence validates the sequence number of an incoming message. It will set raw.sequence if valid.
func msgValidateSequence(raw *MessageRaw, invalidate bool) (valid bool, rtt time.Duration) {
// Only Response and Pong
if raw.Command != CommandResponse && raw.Command != CommandPong {
@@ -124,6 +128,8 @@ func msgValidateSequence(raw *MessageRaw, invalidate bool) (valid bool, rtt time
sequence.expires = time.Now().Add(time.Duration(ReplyTimeout) * time.Second / 2)
}
raw.sequence = sequence
return sequence.expires.After(time.Now()), rtt
}

8
go.mod
View File

@@ -3,12 +3,12 @@ module github.com/PeernetOfficial/core
go 1.16
require (
github.com/PeernetOfficial/core/dht v0.0.0-20210410140655-6a9d16717779
github.com/PeernetOfficial/core/dht v0.0.0-20210421010630-e432696ad116
github.com/btcsuite/btcd v0.21.0-beta.0.20210401013323-36a96f6a0025
github.com/pkg/errors v0.9.1
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/net v0.0.0-20210414194228-064579744ee0
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c
golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc
golang.org/x/net v0.0.0-20210420210106-798c2154c571
golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
lukechampine.com/blake3 v1.1.5
)

18
go.sum
View File

@@ -1,5 +1,5 @@
github.com/PeernetOfficial/core/dht v0.0.0-20210410140655-6a9d16717779 h1:P21VQkgh7qrNuXG/tiXC56BNv2cjGI6z/CYpfrCAVCk=
github.com/PeernetOfficial/core/dht v0.0.0-20210410140655-6a9d16717779/go.mod h1:sb2H21VIVTohCXVrDFo/Skl2tN8Yzba+MXSS6NgCYXw=
github.com/PeernetOfficial/core/dht v0.0.0-20210421010630-e432696ad116 h1:1dqRhS20S6dRPdLL5dqp4TjTWm3vNvSm/EtRpPfxU6U=
github.com/PeernetOfficial/core/dht v0.0.0-20210421010630-e432696ad116/go.mod h1:sb2H21VIVTohCXVrDFo/Skl2tN8Yzba+MXSS6NgCYXw=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.21.0-beta.0.20210401013323-36a96f6a0025 h1:aoVqvZk4mLyF3WZbqEVPq+vXnwL2wekZg4P4mjYJNLs=
@@ -38,22 +38,22 @@ golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc h1:+q90ECDSAQirdykUN6sPEiBXBsp8Csjcca8Oy7bgLTA=
golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210414194228-064579744ee0 h1:iqW3Mjl/6IP9cHJC/wdiIu3lyBDMUfDElRMyFlqbtiQ=
golang.org/x/net v0.0.0-20210414194228-064579744ee0/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210420210106-798c2154c571 h1:Q6Bg8xzKzpFPU4Oi1sBnBTHBwlMsLeEXpu4hYBY8rAg=
golang.org/x/net v0.0.0-20210420210106-798c2154c571/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c h1:6L+uOeS3OQt/f4eFHXZcTxeZrGCuz+CLElgEBjbcTA4=
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988 h1:EjgCl+fVlIaPJSori0ikSz3uV0DOHKWOJFpv1sAAhBM=
golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=