diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index da26fe7a0da14e705c49f6f39443403fbbd58710..0c21d1cb36044213f75594a687aaf9a45e32ca65 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,12 +6,6 @@ cache: paths: - vendor/ -variables: - REPO_DIR: gitlab.com/elixxir - REPO_NAME: primitives - DOCKER_IMAGE: elixxirlabs/cuda-go:latest - MIN_CODE_COVERAGE: "80.0" - before_script: - go version || echo "Go executable not found." - echo $CI_BUILD_REF @@ -21,10 +15,10 @@ before_script: - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null - mkdir -p ~/.ssh - chmod 700 ~/.ssh - - ssh-keyscan -t rsa gitlab.com > ~/.ssh/known_hosts - - git config --global url."git@gitlab.com:".insteadOf "https://gitlab.com/" + - ssh-keyscan -t rsa $GITLAB_SERVER > ~/.ssh/known_hosts + - git config --global url."git@$GITLAB_SERVER:".insteadOf "https://gitlab.com/" - export PATH=$HOME/go/bin:$PATH - - export GOPRIVATE=gitlab.com/elixxir/* + - export GOPRIVATE=gitlab.com/elixxir/*,gitlab.com/xx_network/* stages: - build @@ -32,7 +26,7 @@ stages: build: stage: build - image: $DOCKER_IMAGE + image: elixxirlabs/cuda-go:go1.13-cuda11.1 script: - git clean -ffdx - go mod vendor -v @@ -58,12 +52,11 @@ build: - testdata/ - release/ -trigger: +trigger_integration: stage: trigger_integration - script: - # client - - "curl -X POST -F token=$CLIENT_TRIGGER_KEY -F ref=master https://gitlab.com/api/v4/projects/5454785/trigger/pipeline" - # server - - "curl -X POST -F token=$SERVER_TRIGGER_KEY -F ref=master https://gitlab.com/api/v4/projects/5014439/trigger/pipeline" + trigger: + project: elixxir/integration + branch: $CI_COMMIT_REF_NAME only: + - release - master diff --git a/go.sum b/go.sum index 6bf2385ce6a45d88569e7289b3f385572ec07d47..416d5ffcf1845025c8218c01a71ed28ef09882d0 100644 --- a/go.sum +++ b/go.sum @@ -30,9 +30,11 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -44,6 +46,7 @@ github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/zeebo/assert v0.0.0-20181109011804-10f827ce2ed6/go.mod h1:yssERNPivllc1yU3BvpjYI5BUW+zglcz6QWqeVRL5t0= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= @@ -51,7 +54,6 @@ github.com/zeebo/blake3 v0.0.4/go.mod h1:YOZo8A49yNqM0X/Y+JmDUZshJWLt1laHsNSn5ny github.com/zeebo/pcg v0.0.0-20181207190024-3cdc6b625a05/go.mod h1:Gr+78ptB0MwXxm//LBaEvBiaXY7hXJ6KGe2V32X2F6E= gitlab.com/elixxir/crypto v0.0.0-20200804182833-984246dea2c4/go.mod h1:ucm9SFKJo+K0N2GwRRpaNr+tKXMIOVWzmyUD0SbOu2c= gitlab.com/elixxir/crypto v0.0.3/go.mod h1:ZNgBOblhYToR4m8tj4cMvJ9UsJAUKq+p0gCp07WQmhA= -gitlab.com/elixxir/crypto v0.0.6 h1:c94CGzBTV7LgInGHfmeJHrqq9nIc/WEOLUd9OeQBN74= gitlab.com/elixxir/primitives v0.0.0-20200731184040-494269b53b4d/go.mod h1:OQgUZq7SjnE0b+8+iIAT2eqQF+2IFHn73tOo+aV11mg= gitlab.com/elixxir/primitives v0.0.0-20200804170709-a1896d262cd9/go.mod h1:p0VelQda72OzoUckr1O+vPW0AiFe0nyKQ6gYcmFSuF8= gitlab.com/elixxir/primitives v0.0.0-20200804182913-788f47bded40/go.mod h1:tzdFFvb1ESmuTCOl1z6+yf6oAICDxH2NPUemVgoNLxc= @@ -119,8 +121,10 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/id/ephemeral/id.go b/id/ephemeral/id.go index 598941d75e91fb82693de5fbc254da1d8942fed0..cdbeaa886bea841561de5937c7ba1e589a1fd146 100644 --- a/id/ephemeral/id.go +++ b/id/ephemeral/id.go @@ -1,11 +1,13 @@ package ephemeral import ( + "bytes" "crypto" "encoding/binary" "fmt" "github.com/pkg/errors" "gitlab.com/xx_network/primitives/id" + "hash" "io" "math" "time" @@ -15,6 +17,14 @@ const Period = int64(time.Hour * 24) const NumOffsets int64 = 1 << 16 const NsPerOffset = Period / NumOffsets +// Ephemeral Ids reserved for specific actions: +// All zero's denote a dummy ID +// All one's denote a payment +var ReservedIDs = []Id{ + {0, 0, 0, 0, 0, 0, 0, 0}, + {1, 1, 1, 1, 1, 1, 1, 1}, +} + // Ephemeral ID type alias type Id [8]byte @@ -144,19 +154,51 @@ func GetIdFromIntermediary(iid []byte, size uint, timestamp int64) (Id, time.Tim return Id{}, time.Time{}, time.Time{}, errors.New("Cannot generate ID with size > 64") } salt, start, end := getRotationSalt(iid, timestamp) + + // Continually generate an ephemeral Id until we land on + // an id not within the reserved list of Ids + eid := Id{} + var err error + for reserved := true; reserved; reserved = IsReserved(eid) { + eid, err = getIdFromIntermediaryHelper(b2b, iid, salt, size) + if err != nil { + return Id{}, start, end, err + } + } + return eid, start, end, nil +} + +// Helper function which generates a single ephemeral Id +func getIdFromIntermediaryHelper(b2b hash.Hash, iid, salt []byte, size uint) (Id, error) { + eid := Id{} + _, err := b2b.Write(iid) if err != nil { - return Id{}, start, end, err + return Id{}, err } _, err = b2b.Write(salt) if err != nil { - return Id{}, start, end, err + return Id{}, err } - eid := Id{} + copy(eid[:], b2b.Sum(nil)) + cleared := eid.Clear(size) copy(eid[:], cleared[:]) - return eid, start, end, nil + + return eid, err +} + +// Checks if the Id passed in is among +// the reserved global reserved ID list. +// Returns true if reserved, false if non-reserved +func IsReserved(eid Id) bool { + for _, r := range ReservedIDs { + if bytes.Equal(eid[:], r[:]) { + return true + } + } + return false } // getRotationSalt returns rotation salt based on ID hash and timestamp diff --git a/id/ephemeral/id_test.go b/id/ephemeral/id_test.go index fd452c3c71806a4274254fd48a5cabcaa3163f3b..b6b209dbcd1fef141782d96546b62a9fffb3b5f5 100644 --- a/id/ephemeral/id_test.go +++ b/id/ephemeral/id_test.go @@ -2,11 +2,13 @@ package ephemeral import ( "bytes" + "crypto" "encoding/binary" "gitlab.com/xx_network/crypto/csprng" "gitlab.com/xx_network/primitives/id" _ "golang.org/x/crypto/blake2b" "math" + "strconv" "testing" "time" ) @@ -88,6 +90,87 @@ func TestGetIdFromIntermediary(t *testing.T) { } } +// Check that given precomputed input that should generate a reserved +// ephemeral ID, GetIdFromIntermediary does not generate a reserved Id +func TestGetIdFromIntermediary_Reserved(t *testing.T) { + + // Hardcoded to ensure a collision with a reserved ID + hardcodedTimestamp := int64(1614199942358373731) + size := uint(4) + testId := id.NewIdFromString(strconv.Itoa(41), id.User, t) + + // Intermediary ID expected to generate a reserved ephemeral ID + iid, err := GetIntermediaryId(testId) + if err != nil { + t.Errorf("Failed to get intermediary id: %+v", err) + } + // Generate an ephemeral Id given the input above. This specific + // call does not check if the outputted Id is reserved + salt, _, _ := getRotationSalt(iid, hardcodedTimestamp) + b2b := crypto.BLAKE2b_256.New() + expectedReservedEID, err := getIdFromIntermediaryHelper(b2b, iid, salt, size) + if err != nil { + t.Errorf("Failed to get id from intermediary: %+v", err) + } + + // Check that the ephemeral Id generated with hardcoded data is a reserved ID + if !IsReserved(expectedReservedEID) { + t.Errorf("Expected reserved eid is no longer reserved, " + + "\n\tmay need to find a new ID. Use FindReservedID in this case.") + } + + // Generate an ephemeral ID which given the same input above with the production facing call + eid, _, _, err := GetIdFromIntermediary(iid, size, hardcodedTimestamp) + if err != nil { + t.Errorf("Failed to get id from intermediary: %+v", err) + } + + // Check that the ephemeralID generated is not reserved. + if IsReserved(eid) { + t.Errorf("Ephemeral ID generated should not be reserved!"+ + "\n\tReserved IDs: %v"+ + "\n\tGenerated ID: %v", ReservedIDs, eid) + } + +} + +// Will find a reserved ephemeral ID and returns the +// associated intermediary ID +func FindReservedID(size uint, timestamp int64, t *testing.T) []byte { + b2b := crypto.BLAKE2b_256.New() + + // Loops through until a reserved ID is found + counter := 0 + for { + testId := id.NewIdFromString(strconv.Itoa(counter), id.User, t) + iid, err := GetIntermediaryId(testId) + if err != nil { + t.Errorf("Failed to get intermediary id: %+v", err) + } + + // Generate an ephemeral ID + salt, _, _ := getRotationSalt(iid, timestamp) + eid, err := getIdFromIntermediaryHelper(b2b, iid, salt, size) + if err != nil { + t.Errorf("Failed to get id from intermediary: %+v", err) + } + + // Check if ephemeral ID is reserved exit + if IsReserved(eid) { + t.Logf("Found input which generates a reserved id. Input as follows."+ + "\n\tSize: %d"+ + "\n\tTimestamp: %d"+ + "\n\tTestID: %v"+ + "\n\tTestID generated using the following line of code: "+ + "\n\t\ttestId := id.NewIdFromString(strconv.Itoa(%d), id.User, t)", + size, timestamp, testId, counter) + return iid + } + // Increment the counter + counter++ + } +} + func TestId_Clear(t *testing.T) { eid := Id{} dummyData := []byte{201, 99, 103, 45, 68, 2, 56, 7} diff --git a/ndf/ndf.go b/ndf/ndf.go index a4751677a14eab639b1b9aaa0dc551458c6e0e67..7f3e5a4cd17f75eabdc53fdc0b5cf0336ae83f2a 100644 --- a/ndf/ndf.go +++ b/ndf/ndf.go @@ -4,34 +4,26 @@ // Use of this source code is governed by a license that can be found in the LICENSE file // //////////////////////////////////////////////////////////////////////////////////////////// -// Package ndf contains the structure for our network definition file. This object is used by -// various users, including our cMix nodes and clients of the xx Messenger, among others. -// It also includes functions to unmarshal an NDF from a JSON file, separate the signature -// from the actual NDF content, and serialize the NDF structure into a byte slice +// Package ndf contains the structure for the network definition file. It is +// generated by permissioning and propagates to nodes, gateways, and clients in +// the network. package ndf import ( - "encoding/base64" "encoding/json" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/xx_network/primitives/id" - "strings" "time" ) -// This constant string is to be used by our users that request NDFs from permissioning. -// Those that request include cMix nodes, gateways, notification bot and clients. -// Permissioning builds and provides the ndf to those that request it. However, depending on -// the status of the cMix network, it might not have the the ndf ready upon request. -// Permissioning in this case tells the requester that it is not ready with an error message. -// The requester checks if the error message contains this string, and thus knows it needs to ask -// again. +// NO_NDF is a string that the permissioning server responds with when a member +// of the network requests an NDF from it but the NDF is not yet available. const NO_NDF = "Contacted server does not have an ndf to give" -// NetworkDefinition structure matches the JSON structure generated in -// Terraform, which allows it to be decoded to Go. +// NetworkDefinition structure hold connection and network information. It +// matches the JSON structure generated in Terraform. type NetworkDefinition struct { Timestamp time.Time Gateways []Gateway @@ -42,35 +34,39 @@ type NetworkDefinition struct { E2E Group `json:"E2e"` CMIX Group `json:"Cmix"` AddressSpaceSize uint32 + ClientVersion string } -// Gateway is the structure for the gateways object in the JSON file. +// Gateway contains the connection and identity information of a gateway on the +// network. type Gateway struct { ID []byte `json:"Id"` Address string TlsCertificate string `json:"Tls_certificate"` } -// Node is the structure for the nodes object in the JSON file. +// Node contains the connection and identity information of a node on the +// network. type Node struct { ID []byte `json:"Id"` Address string TlsCertificate string `json:"Tls_certificate"` } -// Registration is the structure for the registration object in the JSON file. +// Registration contains the connection information for the permissioning +// server. type Registration struct { Address string TlsCertificate string `json:"Tls_certificate"` } -// Notifications is the structure for the registration object in the JSON file. +// Notification contains the connection information for the notification bot. type Notification struct { Address string TlsCertificate string `json:"Tls_certificate"` } -// UDB is the structure for the UDB object in the JSON file. +// UDB contains the ID and public key in PEM form for user discovery. type UDB struct { ID []byte `json:"Id"` Cert string `json:"Cert"` @@ -78,8 +74,7 @@ type UDB struct { DhPubKey []byte `json:"DhPubKey"` } -// Group is the structure for a group in the JSON file; it is used for the E2E -// and CMIX objects. +// Group contains the information used to reconstruct a cyclic group. type Group struct { Prime string SmallPrime string `json:"Small_prime"` @@ -95,42 +90,29 @@ func (g *Group) String() (string, error) { return string(data), nil } -// DecodeNDF decodes the given JSON string into the NetworkDefinition structure -// and decodes the base 64 signature to a byte slice. The NDF string is expected -// to have the JSON data on line 1 and its signature on line 2. Returns an error -// if separating the lines fails or if the JSON unmarshal fails. -func DecodeNDF(ndf string) (*NetworkDefinition, []byte, error) { - // Get JSON data and check if the separating failed - jsonData, signature := separate(ndf) - - // Decode the signature form base 64 and check for errors - signatureBytes, err := base64.StdEncoding.DecodeString(signature) - if err != nil { - return nil, nil, err - } - - // Unmarshal the JSON string into a structure - networkDefinition := &NetworkDefinition{} - err = json.Unmarshal([]byte(jsonData), networkDefinition) - if err != nil { - return nil, nil, err - } +// Marshal returns the JSON encoding of the NDF. +func (ndf *NetworkDefinition) Marshal() ([]byte, error) { + return json.Marshal(ndf) +} - return networkDefinition, signatureBytes, nil +// Unmarshal parses the JSON encoded data and returns the resulting +// NetworkDefinition. +func Unmarshal(data []byte) (*NetworkDefinition, error) { + ndf := &NetworkDefinition{} + err := json.Unmarshal(data, ndf) + return ndf, err } -// Returns a stripped down copy of the NDF object to be used by Clients +// StripNdf returns a stripped down copy of the NetworkDefinition to be used by +// Clients. func (ndf *NetworkDefinition) StripNdf() *NetworkDefinition { - // Strip down nodes slice of addresses and certs + // Remove address and TLS cert for every node. var strippedNodes []Node for _, node := range ndf.Nodes { - newNode := Node{ - ID: node.ID, - } - strippedNodes = append(strippedNodes, newNode) + strippedNodes = append(strippedNodes, Node{ID: node.ID}) } - // Create a new Ndf with the stripped information + // Create a new NetworkDefinition with the stripped information return &NetworkDefinition{ Timestamp: ndf.Timestamp, Gateways: ndf.Gateways, @@ -144,34 +126,9 @@ func (ndf *NetworkDefinition) StripNdf() *NetworkDefinition { } } -// separate splits the JSON data from the signature. The NDF string is expected -// to have the JSON data starting on line 1 and its signature on the last line. -// Returns JSON data and signature as separate strings. If the signature is not -// present, it is returned as an empty string. -func separate(ndf string) (string, string) { - var jsonLineEnd int - var signature string - lines := strings.Split(ndf, "\n") - - // Determine which line the JSON ends and which line the signature is on - for i := len(lines) - 1; i >= 0; i-- { - line := strings.TrimSpace(lines[i]) - if line != "" { - if strings.HasSuffix(line, "}") { - jsonLineEnd = i - break - } else { - signature = line - } - } - } - - return strings.Join(lines[0:jsonLineEnd+1], "\n"), signature -} - -// Serialize converts the NetworkDefinition into a byte slice. +// Serialize serializes the NetworkDefinition into a byte slice. func (ndf *NetworkDefinition) Serialize() []byte { - b := make([]byte, 0) + var b []byte // Convert timestamp to a byte slice timeBytes, err := ndf.Timestamp.MarshalBinary() @@ -218,34 +175,12 @@ func (ndf *NetworkDefinition) Serialize() []byte { return b } -// Marshal returns a json marshal of the ndf -func (ndf *NetworkDefinition) Marshal() ([]byte, error) { - ndfBytes, err := json.Marshal(ndf) - if err != nil { - return nil, err - } - - return ndfBytes, nil -} - -// GetNodeId marshals the node id into the ID type. Returns an error if Marshal -// fails. +// GetNodeId unmarshalls the Node's ID bytes into an id.ID and returns it. func (n *Node) GetNodeId() (*id.ID, error) { - newID, err := id.Unmarshal(n.ID) - if err != nil { - return nil, err - } - - return newID, nil - + return id.Unmarshal(n.ID) } -// GetGatewayId formats the gateway id into the id format specified in the id package of this repo -func (n *Gateway) GetGatewayId() (*id.ID, error) { - newID, err := id.Unmarshal(n.ID) - if err != nil { - return nil, err - } - - return newID, nil +// GetGatewayId unmarshalls the Gateway's ID bytes into an id.ID and returns it. +func (g *Gateway) GetGatewayId() (*id.ID, error) { + return id.Unmarshal(g.ID) } diff --git a/ndf/ndf_test.go b/ndf/ndf_test.go index e624a9355209e2c3a56e600f1f8765ceb66bd36f..1bf2a2348d961c407e1e17cc7462110c3351dd8f 100644 --- a/ndf/ndf_test.go +++ b/ndf/ndf_test.go @@ -10,9 +10,8 @@ import ( "bytes" "encoding/base64" "encoding/json" - "errors" + "fmt" "reflect" - "strings" "testing" "time" ) @@ -76,37 +75,27 @@ var ( "Generator": "02" } }` - ExampleSignature = `gkh98J10rQiuVsEXd6xe8IeCINplnD93CFpXZFNjT1CgNMxgsHumiC5HsctjnF0xTxDPq3hn3/J0s+eblSVyGMMszTIoWNINVSS1fkm0EGkKafC1vKTZMmc9ivsWL7oY` - ExampleNDF = ExampleJSON + "\n" + ExampleSignature - JsonBytes, _ = base64.StdEncoding.DecodeString("AQAAAA7UiTKgAAAAAP5cNTIuMjUuMTM1LjUyLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURnVENDQW1tZ0F3SUJBZ0lKQUtMZFo4VWlnSUFlTUEwR0NTcUdTSWIzRFFFQkJRVUFNRzh4Q3pBSkJnTlYKQkFZVEFsVlRNUk13RVFZRFZRUUlEQXBEWVd4cFptOXlibWxoTVJJd0VBWURWUVFIREFsRGJHRnlaVzF2Ym5ReApHekFaQmdOVkJBb01FbEJ5YVhaaGRHVm5jbWwwZVNCRGIzSndMakVhTUJnR0ExVUVBd3dSWjJGMFpYZGhlU291ClkyMXBlQzV5YVhBd0hoY05NVGt3TXpBMU1UZ3pOVFUwV2hjTk1qa3dNekF5TVRnek5UVTBXakJ2TVFzd0NRWUQKVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEyeGhjbVZ0YjI1MApNUnN3R1FZRFZRUUtEQkpRY21sMllYUmxaM0pwZEhrZ1EyOXljQzR4R2pBWUJnTlZCQU1NRVdkaGRHVjNZWGtxCkxtTnRhWGd1Y21sd01JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOStBYXh3RFAKeEhiaExtbjRIb1p1MG9VTTQ4UXVmYzZUNVhFWlRycE1ycUpBb3VYays2MUpjMEVGSDk2L3NiajdWeXZuWFBSbwpnSUVOYmsyWTg0QmtCOVNrUk1JWHlhL2doOWRPRURTZ252ai95ZzI0bDNiZEtGcUJNS2lGZzAwUFlCMzBmVStBCmJlM09JL2xlMEkrdisrUndIMkFWMEJNcStUNlBjQUdqQ0MxUTFaQjB3UDkvVnFOTVdxNWxiSzl3RDQ2SVFpU2kKK1NnSVFlRTdIb2lBWlhyR08wWTdsOVAzK1ZSb1hqUlFicWZuM0VUTkw5WnZRdWFyd0FZQzlJeDVNeFVyUzVhZwpPbWZqYzhiZmtwWURGQVhSWG1kS05JU0ptdENlYlgya0RycFA4QmRhc3g3RnpzeDU5Y0VVSENsMmFKT1dYYzdSCjVtM2p1T1ZMMUhVeGpRSURBUUFCb3lBd0hqQWNCZ05WSFJFRUZUQVRnaEZuWVhSbGQyRjVLaTVqYldsNExuSnAKY0RBTkJna3Foa2lHOXcwQkFRVUZBQU9DQVFFQU11M3hvYzJMVzJVRXhBQUlZWVdFRVRnZ0xOcmxHb254dGVTdQpqdUpqT1IraWs1U1ZMbjBsRXUyMit6K0ZDQTdnU2s5RmtXdSt2OXFuZk9mbTJBbStXS1lXdjNkSjVSeXBXL2hECk5Ya09ZeFZKTllGeGVTaG5Ib2hOcXE0ZURLcGRxU3hFY3VFckZYSmRMYlpQMXVOczRXSU9LblRoZ3poa3B1eTcKdFpSb3N2T0YxWDV1TDFmclZKekhONWpBU0VEQWE3aEpObVEyNGtoK2RzL0dlMzlmR0Q4cEszMUNXaG5JWGVEbwp2S0Q3d2l2aS9nU09CdGNSV1dMdlU4U2l6WmtTM2hnVHcwbFNPZjVnZXV6dmFzQ0VZbHFyS0Zzc2o2Y1R6YkNCCnh5M3JhM1dhelJUTlRXNFRta0hsQ1VDOUkzb1dUVHh3NWlReEYvSTJrUVFud1I3TDN3PT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLTUyLjI1LjIxOS4zOC0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlEZ1RDQ0FtbWdBd0lCQWdJSkFLTGRaOFVpZ0lBZU1BMEdDU3FHU0liM0RRRUJCUVVBTUc4eEN6QUpCZ05WCkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbERiR0Z5WlcxdmJuUXgKR3pBWkJnTlZCQW9NRWxCeWFYWmhkR1ZuY21sMGVTQkRiM0p3TGpFYU1CZ0dBMVVFQXd3UloyRjBaWGRoZVNvdQpZMjFwZUM1eWFYQXdIaGNOTVRrd016QTFNVGd6TlRVMFdoY05Namt3TXpBeU1UZ3pOVFUwV2pCdk1Rc3dDUVlEClZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVNNQkFHQTFVRUJ3d0pRMnhoY21WdGIyNTAKTVJzd0dRWURWUVFLREJKUWNtbDJZWFJsWjNKcGRIa2dRMjl5Y0M0eEdqQVlCZ05WQkFNTUVXZGhkR1YzWVhrcQpMbU50YVhndWNtbHdNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQTkrQWF4d0RQCnhIYmhMbW40SG9adTBvVU00OFF1ZmM2VDVYRVpUcnBNcnFKQW91WGsrNjFKYzBFRkg5Ni9zYmo3Vnl2blhQUm8KZ0lFTmJrMlk4NEJrQjlTa1JNSVh5YS9naDlkT0VEU2dudmoveWcyNGwzYmRLRnFCTUtpRmcwMFBZQjMwZlUrQQpiZTNPSS9sZTBJK3YrK1J3SDJBVjBCTXErVDZQY0FHakNDMVExWkIwd1A5L1ZxTk1XcTVsYks5d0Q0NklRaVNpCitTZ0lRZUU3SG9pQVpYckdPMFk3bDlQMytWUm9YalJRYnFmbjNFVE5MOVp2UXVhcndBWUM5SXg1TXhVclM1YWcKT21mamM4YmZrcFlERkFYUlhtZEtOSVNKbXRDZWJYMmtEcnBQOEJkYXN4N0Z6c3g1OWNFVUhDbDJhSk9XWGM3Ugo1bTNqdU9WTDFIVXhqUUlEQVFBQm95QXdIakFjQmdOVkhSRUVGVEFUZ2hGbllYUmxkMkY1S2k1amJXbDRMbkpwCmNEQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUFNdTN4b2MyTFcyVUV4QUFJWVlXRUVUZ2dMTnJsR29ueHRlU3UKanVKak9SK2lrNVNWTG4wbEV1MjIreitGQ0E3Z1NrOUZrV3UrdjlxbmZPZm0yQW0rV0tZV3YzZEo1UnlwVy9oRApOWGtPWXhWSk5ZRnhlU2huSG9oTnFxNGVES3BkcVN4RWN1RXJGWEpkTGJaUDF1TnM0V0lPS25UaGd6aGtwdXk3CnRaUm9zdk9GMVg1dUwxZnJWSnpITjVqQVNFREFhN2hKTm1RMjRraCtkcy9HZTM5ZkdEOHBLMzFDV2huSVhlRG8KdktEN3dpdmkvZ1NPQnRjUldXTHZVOFNpelprUzNoZ1R3MGxTT2Y1Z2V1enZhc0NFWWxxcktGc3NqNmNUemJDQgp4eTNyYTNXYXpSVE5UVzRUbWtIbENVQzlJM29XVFR4dzVpUXhGL0kya1FRbndSN0wzdz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS01Mi40MS44MC4xMDQtLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJRGdUQ0NBbW1nQXdJQkFnSUpBS0xkWjhVaWdJQWVNQTBHQ1NxR1NJYjNEUUVCQlFVQU1HOHhDekFKQmdOVgpCQVlUQWxWVE1STXdFUVlEVlFRSURBcERZV3hwWm05eWJtbGhNUkl3RUFZRFZRUUhEQWxEYkdGeVpXMXZiblF4Ckd6QVpCZ05WQkFvTUVsQnlhWFpoZEdWbmNtbDBlU0JEYjNKd0xqRWFNQmdHQTFVRUF3d1JaMkYwWlhkaGVTb3UKWTIxcGVDNXlhWEF3SGhjTk1Ua3dNekExTVRnek5UVTBXaGNOTWprd016QXlNVGd6TlRVMFdqQnZNUXN3Q1FZRApWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTJ4aGNtVnRiMjUwCk1Sc3dHUVlEVlFRS0RCSlFjbWwyWVhSbFozSnBkSGtnUTI5eWNDNHhHakFZQmdOVkJBTU1FV2RoZEdWM1lYa3EKTG1OdGFYZ3VjbWx3TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUE5K0FheHdEUAp4SGJoTG1uNEhvWnUwb1VNNDhRdWZjNlQ1WEVaVHJwTXJxSkFvdVhrKzYxSmMwRUZIOTYvc2JqN1Z5dm5YUFJvCmdJRU5iazJZODRCa0I5U2tSTUlYeWEvZ2g5ZE9FRFNnbnZqL3lnMjRsM2JkS0ZxQk1LaUZnMDBQWUIzMGZVK0EKYmUzT0kvbGUwSSt2KytSd0gyQVYwQk1xK1Q2UGNBR2pDQzFRMVpCMHdQOS9WcU5NV3E1bGJLOXdENDZJUWlTaQorU2dJUWVFN0hvaUFaWHJHTzBZN2w5UDMrVlJvWGpSUWJxZm4zRVROTDladlF1YXJ3QVlDOUl4NU14VXJTNWFnCk9tZmpjOGJma3BZREZBWFJYbWRLTklTSm10Q2ViWDJrRHJwUDhCZGFzeDdGenN4NTljRVVIQ2wyYUpPV1hjN1IKNW0zanVPVkwxSFV4alFJREFRQUJveUF3SGpBY0JnTlZIUkVFRlRBVGdoRm5ZWFJsZDJGNUtpNWpiV2w0TG5KcApjREFOQmdrcWhraUc5dzBCQVFVRkFBT0NBUUVBTXUzeG9jMkxXMlVFeEFBSVlZV0VFVGdnTE5ybEdvbnh0ZVN1Cmp1SmpPUitpazVTVkxuMGxFdTIyK3orRkNBN2dTazlGa1d1K3Y5cW5mT2ZtMkFtK1dLWVd2M2RKNVJ5cFcvaEQKTlhrT1l4VkpOWUZ4ZVNobkhvaE5xcTRlREtwZHFTeEVjdUVyRlhKZExiWlAxdU5zNFdJT0tuVGhnemhrcHV5Nwp0WlJvc3ZPRjFYNXVMMWZyVkp6SE41akFTRURBYTdoSk5tUTI0a2grZHMvR2UzOWZHRDhwSzMxQ1dobklYZURvCnZLRDd3aXZpL2dTT0J0Y1JXV0x2VThTaXpaa1MzaGdUdzBsU09mNWdldXp2YXNDRVlscXJLRnNzajZjVHpiQ0IKeHkzcmEzV2F6UlROVFc0VG1rSGxDVUM5STNvV1RUeHc1aVF4Ri9JMmtRUW53UjdMM3c9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMTguMjM3LjE0Ny4xMDUtLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJRGJEQ0NBbFNnQXdJQkFnSUpBT1VOdFpuZUlZRUNNQTBHQ1NxR1NJYjNEUUVCQlFVQU1HZ3hDekFKQmdOVgpCQVlUQWxWVE1STXdFUVlEVlFRSURBcERZV3hwWm05eWJtbGhNUkl3RUFZRFZRUUhEQWxEYkdGeVpXMXZiblF4Ckd6QVpCZ05WQkFvTUVsQnlhWFpoZEdWbmNtbDBlU0JEYjNKd0xqRVRNQkVHQTFVRUF3d0tLaTVqYldsNExuSnAKY0RBZUZ3MHhPVEF6TURVeE9ETTFORE5hRncweU9UQXpNREl4T0RNMU5ETmFNR2d4Q3pBSkJnTlZCQVlUQWxWVApNUk13RVFZRFZRUUlEQXBEWVd4cFptOXlibWxoTVJJd0VBWURWUVFIREFsRGJHRnlaVzF2Ym5ReEd6QVpCZ05WCkJBb01FbEJ5YVhaaGRHVm5jbWwwZVNCRGIzSndMakVUTUJFR0ExVUVBd3dLS2k1amJXbDRMbkpwY0RDQ0FTSXcKRFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQVBQMFd5VmtmWkEvQ0VkMkRnS3BjdWRuMG9EaApEd3NqbXg4TEJEV3NVZ1F6eUxyRmlWaWdmVW1VZWZrblVIM2RUSmptaUp0R3FMc2F5Q25XZHFXTEhQSll2RmZzCldZVzBJR0Y5M1VHLzRONVVBV080b2tDM0NZZ0tTaTRla3BmdzJ6Z1pxMGdtYnpUblhjSEY5Z2ZtUTdqSlVLU0UKdEpQU056WHErUFplSlRDOXpKQWI0TGo4UXpIMThyRE04RGFMMnkxbnMwWTJIdTBlZEJGbi9PcWF2QkpLYi91QQptM0FFanFlT2hDN0VRVWpWYW1XbFRCUHQ0MCtCLzZhRkpYNUJZbTJKRmtSc0dCSXlCVkw0Nk12QzAyTWd6VFQ5CmJKSUpmd3FtQmFUcnV3ZW1OZ3pHdTdKazAzaHFxUzFUVUV2U0k2L3g4YlZvYmEzb3JjS2tmOUhzRGpFQ0F3RUEKQWFNWk1CY3dGUVlEVlIwUkJBNHdESUlLS2k1amJXbDRMbkpwY0RBTkJna3Foa2lHOXcwQkFRVUZBQU9DQVFFQQpuZVVvY040QWJjUUFDMStiM1RvOHU1VUdkYUd4aGNHeVpCbEFvZW5SVmRqWEszbFRqc01kTVdiNFFjdGdOZklmClUvenVVbjJteFRtRi9la1AwZ0NDZ3RsZVpyOStEWUtVNWhsWGs4SzEwdUt4R0Q2RXZvaVhaemxmZVV1b3RncDIKcXZJM3lzT20vaHZDZnlFa3FoZkh0YnhqVjdqN3Y3ZVFGUGJ2TmFYYkxhMHlyNEM0dk1LL1owOVVpOUpyWi9aNApjeUlreGZDNi9yT3FBaXJTZElwMDlFR2l3N0dNOGd1SHlnZ0U0SWlackRzbFQ4VjN4SWw5ODVjYkN4U3hlVzFSCnRnSDRyZEVYdVZlOSszMW9KaG1YT0U5dXgyakNvcDl0RUpNZ1dnN0hTdHJKNXBsUGJiK0htam9YM25CTzA0RTUKNm01MlB5ek1OVisyTjIxSVBwcEt3QT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1Mi4xMS4xMzYuMjM4LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURiRENDQWxTZ0F3SUJBZ0lKQU9VTnRabmVJWUVDTUEwR0NTcUdTSWIzRFFFQkJRVUFNR2d4Q3pBSkJnTlYKQkFZVEFsVlRNUk13RVFZRFZRUUlEQXBEWVd4cFptOXlibWxoTVJJd0VBWURWUVFIREFsRGJHRnlaVzF2Ym5ReApHekFaQmdOVkJBb01FbEJ5YVhaaGRHVm5jbWwwZVNCRGIzSndMakVUTUJFR0ExVUVBd3dLS2k1amJXbDRMbkpwCmNEQWVGdzB4T1RBek1EVXhPRE0xTkROYUZ3MHlPVEF6TURJeE9ETTFORE5hTUdneEN6QUpCZ05WQkFZVEFsVlQKTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbERiR0Z5WlcxdmJuUXhHekFaQmdOVgpCQW9NRWxCeWFYWmhkR1ZuY21sMGVTQkRiM0p3TGpFVE1CRUdBMVVFQXd3S0tpNWpiV2w0TG5KcGNEQ0NBU0l3CkRRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFQUDBXeVZrZlpBL0NFZDJEZ0twY3VkbjBvRGgKRHdzam14OExCRFdzVWdRenlMckZpVmlnZlVtVWVma25VSDNkVEpqbWlKdEdxTHNheUNuV2RxV0xIUEpZdkZmcwpXWVcwSUdGOTNVRy80TjVVQVdPNG9rQzNDWWdLU2k0ZWtwZncyemdacTBnbWJ6VG5YY0hGOWdmbVE3akpVS1NFCnRKUFNOelhxK1BaZUpUQzl6SkFiNExqOFF6SDE4ckRNOERhTDJ5MW5zMFkySHUwZWRCRm4vT3FhdkJKS2IvdUEKbTNBRWpxZU9oQzdFUVVqVmFtV2xUQlB0NDArQi82YUZKWDVCWW0ySkZrUnNHQkl5QlZMNDZNdkMwMk1nelRUOQpiSklKZndxbUJhVHJ1d2VtTmd6R3U3SmswM2hxcVMxVFVFdlNJNi94OGJWb2JhM29yY0trZjlIc0RqRUNBd0VBCkFhTVpNQmN3RlFZRFZSMFJCQTR3RElJS0tpNWpiV2w0TG5KcGNEQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUEKbmVVb2NONEFiY1FBQzErYjNUbzh1NVVHZGFHeGhjR3laQmxBb2VuUlZkalhLM2xUanNNZE1XYjRRY3RnTmZJZgpVL3p1VW4ybXhUbUYvZWtQMGdDQ2d0bGVacjkrRFlLVTVobFhrOEsxMHVLeEdENkV2b2lYWnpsZmVVdW90Z3AyCnF2STN5c09tL2h2Q2Z5RWtxaGZIdGJ4alY3ajd2N2VRRlBidk5hWGJMYTB5cjRDNHZNSy9aMDlVaTlKclovWjQKY3lJa3hmQzYvck9xQWlyU2RJcDA5RUdpdzdHTThndUh5Z2dFNElpWnJEc2xUOFYzeElsOTg1Y2JDeFN4ZVcxUgp0Z0g0cmRFWHVWZTkrMzFvSmhtWE9FOXV4MmpDb3A5dEVKTWdXZzdIU3RySjVwbFBiYitIbWpvWDNuQk8wNEU1CjZtNTJQeXpNTlYrMk4yMUlQcHBLd0E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMzQuMjEzLjc5LjMxLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURiRENDQWxTZ0F3SUJBZ0lKQU9VTnRabmVJWUVDTUEwR0NTcUdTSWIzRFFFQkJRVUFNR2d4Q3pBSkJnTlYKQkFZVEFsVlRNUk13RVFZRFZRUUlEQXBEWVd4cFptOXlibWxoTVJJd0VBWURWUVFIREFsRGJHRnlaVzF2Ym5ReApHekFaQmdOVkJBb01FbEJ5YVhaaGRHVm5jbWwwZVNCRGIzSndMakVUTUJFR0ExVUVBd3dLS2k1amJXbDRMbkpwCmNEQWVGdzB4T1RBek1EVXhPRE0xTkROYUZ3MHlPVEF6TURJeE9ETTFORE5hTUdneEN6QUpCZ05WQkFZVEFsVlQKTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbERiR0Z5WlcxdmJuUXhHekFaQmdOVgpCQW9NRWxCeWFYWmhkR1ZuY21sMGVTQkRiM0p3TGpFVE1CRUdBMVVFQXd3S0tpNWpiV2w0TG5KcGNEQ0NBU0l3CkRRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFQUDBXeVZrZlpBL0NFZDJEZ0twY3VkbjBvRGgKRHdzam14OExCRFdzVWdRenlMckZpVmlnZlVtVWVma25VSDNkVEpqbWlKdEdxTHNheUNuV2RxV0xIUEpZdkZmcwpXWVcwSUdGOTNVRy80TjVVQVdPNG9rQzNDWWdLU2k0ZWtwZncyemdacTBnbWJ6VG5YY0hGOWdmbVE3akpVS1NFCnRKUFNOelhxK1BaZUpUQzl6SkFiNExqOFF6SDE4ckRNOERhTDJ5MW5zMFkySHUwZWRCRm4vT3FhdkJKS2IvdUEKbTNBRWpxZU9oQzdFUVVqVmFtV2xUQlB0NDArQi82YUZKWDVCWW0ySkZrUnNHQkl5QlZMNDZNdkMwMk1nelRUOQpiSklKZndxbUJhVHJ1d2VtTmd6R3U3SmswM2hxcVMxVFVFdlNJNi94OGJWb2JhM29yY0trZjlIc0RqRUNBd0VBCkFhTVpNQmN3RlFZRFZSMFJCQTR3RElJS0tpNWpiV2w0TG5KcGNEQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUEKbmVVb2NONEFiY1FBQzErYjNUbzh1NVVHZGFHeGhjR3laQmxBb2VuUlZkalhLM2xUanNNZE1XYjRRY3RnTmZJZgpVL3p1VW4ybXhUbUYvZWtQMGdDQ2d0bGVacjkrRFlLVTVobFhrOEsxMHVLeEdENkV2b2lYWnpsZmVVdW90Z3AyCnF2STN5c09tL2h2Q2Z5RWtxaGZIdGJ4alY3ajd2N2VRRlBidk5hWGJMYTB5cjRDNHZNSy9aMDlVaTlKclovWjQKY3lJa3hmQzYvck9xQWlyU2RJcDA5RUdpdzdHTThndUh5Z2dFNElpWnJEc2xUOFYzeElsOTg1Y2JDeFN4ZVcxUgp0Z0g0cmRFWHVWZTkrMzFvSmhtWE9FOXV4MmpDb3A5dEVKTWdXZzdIU3RySjVwbFBiYitIbWpvWDNuQk8wNEU1CjZtNTJQeXpNTlYrMk4yMUlQcHBLd0E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tcmVnaXN0cmF0aW9uLmRlZmF1bHQuY21peC5yaXAtLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJRGtEQ0NBbmlnQXdJQkFnSUpBSm5qb3N1U3NQN2dNQTBHQ1NxR1NJYjNEUUVCQlFVQU1IUXhDekFKQmdOVgpCQVlUQWxWVE1STXdFUVlEVlFRSURBcERZV3hwWm05eWJtbGhNUkl3RUFZRFZRUUhEQWxEYkdGeVpXMXZiblF4Ckd6QVpCZ05WQkFvTUVsQnlhWFpoZEdWbmNtbDBlU0JEYjNKd0xqRWZNQjBHQTFVRUF3d1djbVZuYVhOMGNtRjAKYVc5dUtpNWpiV2w0TG5KcGNEQWVGdzB4T1RBek1EVXlNVFE1TlRaYUZ3MHlPVEF6TURJeU1UUTVOVFphTUhReApDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbERiR0Z5ClpXMXZiblF4R3pBWkJnTlZCQW9NRWxCeWFYWmhkR1ZuY21sMGVTQkRiM0p3TGpFZk1CMEdBMVVFQXd3V2NtVm4KYVhOMGNtRjBhVzl1S2k1amJXbDRMbkpwY0RDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQwpnZ0VCQU9RS3ZxamRoMzVvK01FQ0JoQ3dvcEp6UGxRTm1xMmlQYmV3Uk50STAyYlVOSzNrTFFVYkZsWWR6TkdaClM0R1lYR2M1TytqZGk4U2x4ODJyMWtkano1UFBDTkZCQVJJc09QL0w4cjNER2VXK3llSmRnQlpqbTFzM3lsa2EKbXQ0QWppcS9iTmp5c1M2TC9XU09wK3NWdW1EeHRCRXpPL1VUVTFPNlFSbnpVcGhMYWlXRU5tRXJHdnNIMENaVgpxMzhJYTU4ay9RakNBenBVY1lpNGoybDFmYjA3eHFGY1FEOEg2U21VTTI5N1V5UW9zRHJwOHVrZElvMzFLb3hyCjRYRG5uTk5zWVN0QzI2dHpITWVLdUoyV2wrM1l6c1N5ZmxmTTJZRWNLRTMxc3FCOURTMzZVa0o4Sjg0ZUxzSE4KSW1HZzNXb2RGQXZpREI2NytqWERiQjMwTmtNQ0F3RUFBYU1sTUNNd0lRWURWUjBSQkJvd0dJSVdjbVZuYVhOMApjbUYwYVc5dUtpNWpiV2w0TG5KcGNEQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUFGOW1OemsrZytvNjI2UmxsCnQzZjMvMXFJeVlRcllKMEJqU1dDS1lFRk1DZ1o0SmliQUpqQXZJYWpoVllFUnRsdGZmTStZS2NkRTJrVHBkekoKMFlKdVVuUmZ1djZzVm5YbFZWdWdVVW5kNElPaWdtamJDZE0zMmsxNzBDWU1tMGFpd0d4bDRGck5hOGVpN0FJYQp4L3MxbitzcVdxM0hlVzVMWGpub1ZiK3MzSGVDV0l1TGZjZ3J1cmZ5ZThGbk5oeTE0SEZ6eFZZWWVmSUttMFhMCitEUGxjR0dHbS9QUFl0M3U0YTIrclAzeGFpaGM2NWRUYTB1NXRmL1hQWHRQeFREUEZqMkplUURGeG83UVJSRWIKUEQ4OUN0WW53dVA5MzdDcmt2Q0tyTDBHa1cxRlZpWEtxWlk5RjV1aHhydkxJcHpoYk5ycy9FYnR3ZVkzNVhHTApEQ0NNa2c9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwdWJrZXkxLjIuMy40OjEyMzQBAgNGRkZGRkZGRkZGRkZGRkZGQzkwRkRBQTIyMTY4QzIzNEM0QzY2MjhCODBEQzFDRDEyOTAyNEUwODhBNjdDQzc0MDIwQkJFQTYzQjEzOUIyMjUxNEEwODc5OEUzNDA0RERFRjk1MTlCM0NEM0E0MzFCMzAyQjBBNkRGMjVGMTQzNzRGRTEzNTZENkQ1MUMyNDVFNDg1QjU3NjYyNUU3RUM2RjQ0QzQyRTlBNjM3RUQ2QjBCRkY1Q0I2RjQwNkI3RURFRTM4NkJGQjVBODk5RkE1QUU5RjI0MTE3QzRCMUZFNjQ5Mjg2NjUxRUNFNDVCM0RDMjAwN0NCOEExNjNCRjA1OThEQTQ4MzYxQzU1RDM5QTY5MTYzRkE4RkQyNENGNUY4MzY1NUQyM0RDQTNBRDk2MUM2MkYzNTYyMDg1NTJCQjlFRDUyOTA3NzA5Njk2NkQ2NzBDMzU0RTRBQkM5ODA0RjE3NDZDMDhDQTE4MjE3QzMyOTA1RTQ2MkUzNkNFM0JFMzlFNzcyQzE4MEU4NjAzOUIyNzgzQTJFQzA3QTI4RkI1QzU1REYwNkY0QzUyQzlERTJCQ0JGNjk1NTgxNzE4Mzk5NTQ5N0NFQTk1NkFFNTE1RDIyNjE4OThGQTA1MTAxNTcyOEU1QThBQUNBQTY4RkZGRkZGRkZGRkZGRkZGRjAyN0ZGRkZGRkZGRkZGRkZGRkU0ODdFRDUxMTBCNDYxMUE2MjYzMzE0NUMwNkUwRTY4OTQ4MTI3MDQ0NTMzRTYzQTAxMDVERjUzMUQ4OUNEOTEyOEE1MDQzQ0M3MUEwMjZFRjdDQThDRDlFNjlEMjE4RDk4MTU4NTM2RjkyRjhBMUJBN0YwOUFCNkI2QThFMTIyRjI0MkRBQkIzMTJGM0Y2MzdBMjYyMTc0RDMxQkY2QjU4NUZGQUU1QjdBMDM1QkY2RjcxQzM1RkRBRDQ0Q0ZEMkQ3NEY5MjA4QkUyNThGRjMyNDk0MzMyOEY2NzIyRDlFRTEwMDNFNUM1MEIxREY4MkNDNkQyNDFCMEUyQUU5Q0QzNDhCMUZENDdFOTI2N0FGQzFCMkFFOTFFRTUxRDZDQjBFMzE3OUFCMTA0MkE5NURDRjZBOTQ4M0I4NEI0QjM2QjM4NjFBQTcyNTVFNEMwMjc4QkEzNjA0NjUwQzEwQkUxOTQ4MkYyMzE3MUI2NzFERjFDRjNCOTYwQzA3NDMwMUNEOTNDMUQxNzYwM0QxNDdEQUUyQUVGODM3QTYyOTY0RUYxNUU1RkI0QUFDMEI4QzFDQ0FBNEJFNzU0QUI1NzI4QUU5MTMwQzRDN0QwMjg4MEFCOTQ3MkQ0NTU2NTUzNDdGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQzkwRkRBQTIyMTY4QzIzNEM0QzY2MjhCODBEQzFDRDEyOTAyNEUwODhBNjdDQzc0MDIwQkJFQTYzQjEzOUIyMjUxNEEwODc5OEUzNDA0RERFRjk1MTlCM0NEM0E0MzFCMzAyQjBBNkRGMjVGMTQzNzRGRTEzNTZENkQ1MUMyNDVFNDg1QjU3NjYyNUU3RUM2RjQ0QzQyRTlBNjM3RUQ2QjBCRkY1Q0I2RjQwNkI3RURFRTM4NkJGQjVBODk5RkE1QUU5RjI0MTE3QzRCMUZFNjQ5Mjg2NjUxRUNFNDVCM0RDMjAwN0NCOEExNjNCRjA1OThEQTQ4MzYxQzU1RDM5QTY5MTYzRkE4RkQyNENGNUY4MzY1NUQyM0RDQTNBRDk2MUM2MkYzNTYyMDg1NTJCQjlFRDUyOTA3NzA5Njk2NkQ2NzBDMzU0RTRBQkM5ODA0RjE3NDZDMDhDQTE4MjE3QzMyOTA1RTQ2MkUzNkNFM0JFMzlFNzcyQzE4MEU4NjAzOUIyNzgzQTJFQzA3QTI4RkI1QzU1REYwNkY0QzUyQzlERTJCQ0JGNjk1NTgxNzE4Mzk5NTQ5N0NFQTk1NkFFNTE1RDIyNjE4OThGQTA1MTAxNTcyOEU1QThBQUNBQTY4RkZGRkZGRkZGRkZGRkZGRjAyN0ZGRkZGRkZGRkZGRkZGRkU0ODdFRDUxMTBCNDYxMUE2MjYzMzE0NUMwNkUwRTY4OTQ4MTI3MDQ0NTMzRTYzQTAxMDVERjUzMUQ4OUNEOTEyOEE1MDQzQ0M3MUEwMjZFRjdDQThDRDlFNjlEMjE4RDk4MTU4NTM2RjkyRjhBMUJBN0YwOUFCNkI2QThFMTIyRjI0MkRBQkIzMTJGM0Y2MzdBMjYyMTc0RDMxQkY2QjU4NUZGQUU1QjdBMDM1QkY2RjcxQzM1RkRBRDQ0Q0ZEMkQ3NEY5MjA4QkUyNThGRjMyNDk0MzMyOEY2NzIyRDlFRTEwMDNFNUM1MEIxREY4MkNDNkQyNDFCMEUyQUU5Q0QzNDhCMUZENDdFOTI2N0FGQzFCMkFFOTFFRTUxRDZDQjBFMzE3OUFCMTA0MkE5NURDRjZBOTQ4M0I4NEI0QjM2QjM4NjFBQTcyNTVFNEMwMjc4QkEzNjA0NjUwQzEwQkUxOTQ4MkYyMzE3MUI2NzFERjFDRjNCOTYwQzA3NDMwMUNEOTNDMUQxNzYwM0QxNDdEQUUyQUVGODM3QTYyOTY0RUYxNUU1RkI0QUFDMEI4QzFDQ0FBNEJFNzU0QUI1NzI4QUU5MTMwQzRDN0QwMjg4MEFCOTQ3MkQ0NTU2NTUzNDdGRkZGRkZGRkZGRkZGRkY=") - SignatureBytes, _ = base64.StdEncoding.DecodeString(ExampleSignature) + ExampleNDF = ExampleJSON + JsonBytes, _ = base64.StdEncoding.DecodeString("AQAAAA7UiTKgAAAAAP5cNTIuMjUuMTM1LjUyLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURnVENDQW1tZ0F3SUJBZ0lKQUtMZFo4VWlnSUFlTUEwR0NTcUdTSWIzRFFFQkJRVUFNRzh4Q3pBSkJnTlYKQkFZVEFsVlRNUk13RVFZRFZRUUlEQXBEWVd4cFptOXlibWxoTVJJd0VBWURWUVFIREFsRGJHRnlaVzF2Ym5ReApHekFaQmdOVkJBb01FbEJ5YVhaaGRHVm5jbWwwZVNCRGIzSndMakVhTUJnR0ExVUVBd3dSWjJGMFpYZGhlU291ClkyMXBlQzV5YVhBd0hoY05NVGt3TXpBMU1UZ3pOVFUwV2hjTk1qa3dNekF5TVRnek5UVTBXakJ2TVFzd0NRWUQKVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEyeGhjbVZ0YjI1MApNUnN3R1FZRFZRUUtEQkpRY21sMllYUmxaM0pwZEhrZ1EyOXljQzR4R2pBWUJnTlZCQU1NRVdkaGRHVjNZWGtxCkxtTnRhWGd1Y21sd01JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOStBYXh3RFAKeEhiaExtbjRIb1p1MG9VTTQ4UXVmYzZUNVhFWlRycE1ycUpBb3VYays2MUpjMEVGSDk2L3NiajdWeXZuWFBSbwpnSUVOYmsyWTg0QmtCOVNrUk1JWHlhL2doOWRPRURTZ252ai95ZzI0bDNiZEtGcUJNS2lGZzAwUFlCMzBmVStBCmJlM09JL2xlMEkrdisrUndIMkFWMEJNcStUNlBjQUdqQ0MxUTFaQjB3UDkvVnFOTVdxNWxiSzl3RDQ2SVFpU2kKK1NnSVFlRTdIb2lBWlhyR08wWTdsOVAzK1ZSb1hqUlFicWZuM0VUTkw5WnZRdWFyd0FZQzlJeDVNeFVyUzVhZwpPbWZqYzhiZmtwWURGQVhSWG1kS05JU0ptdENlYlgya0RycFA4QmRhc3g3RnpzeDU5Y0VVSENsMmFKT1dYYzdSCjVtM2p1T1ZMMUhVeGpRSURBUUFCb3lBd0hqQWNCZ05WSFJFRUZUQVRnaEZuWVhSbGQyRjVLaTVqYldsNExuSnAKY0RBTkJna3Foa2lHOXcwQkFRVUZBQU9DQVFFQU11M3hvYzJMVzJVRXhBQUlZWVdFRVRnZ0xOcmxHb254dGVTdQpqdUpqT1IraWs1U1ZMbjBsRXUyMit6K0ZDQTdnU2s5RmtXdSt2OXFuZk9mbTJBbStXS1lXdjNkSjVSeXBXL2hECk5Ya09ZeFZKTllGeGVTaG5Ib2hOcXE0ZURLcGRxU3hFY3VFckZYSmRMYlpQMXVOczRXSU9LblRoZ3poa3B1eTcKdFpSb3N2T0YxWDV1TDFmclZKekhONWpBU0VEQWE3aEpObVEyNGtoK2RzL0dlMzlmR0Q4cEszMUNXaG5JWGVEbwp2S0Q3d2l2aS9nU09CdGNSV1dMdlU4U2l6WmtTM2hnVHcwbFNPZjVnZXV6dmFzQ0VZbHFyS0Zzc2o2Y1R6YkNCCnh5M3JhM1dhelJUTlRXNFRta0hsQ1VDOUkzb1dUVHh3NWlReEYvSTJrUVFud1I3TDN3PT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLTUyLjI1LjIxOS4zOC0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlEZ1RDQ0FtbWdBd0lCQWdJSkFLTGRaOFVpZ0lBZU1BMEdDU3FHU0liM0RRRUJCUVVBTUc4eEN6QUpCZ05WCkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbERiR0Z5WlcxdmJuUXgKR3pBWkJnTlZCQW9NRWxCeWFYWmhkR1ZuY21sMGVTQkRiM0p3TGpFYU1CZ0dBMVVFQXd3UloyRjBaWGRoZVNvdQpZMjFwZUM1eWFYQXdIaGNOTVRrd016QTFNVGd6TlRVMFdoY05Namt3TXpBeU1UZ3pOVFUwV2pCdk1Rc3dDUVlEClZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVNNQkFHQTFVRUJ3d0pRMnhoY21WdGIyNTAKTVJzd0dRWURWUVFLREJKUWNtbDJZWFJsWjNKcGRIa2dRMjl5Y0M0eEdqQVlCZ05WQkFNTUVXZGhkR1YzWVhrcQpMbU50YVhndWNtbHdNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQTkrQWF4d0RQCnhIYmhMbW40SG9adTBvVU00OFF1ZmM2VDVYRVpUcnBNcnFKQW91WGsrNjFKYzBFRkg5Ni9zYmo3Vnl2blhQUm8KZ0lFTmJrMlk4NEJrQjlTa1JNSVh5YS9naDlkT0VEU2dudmoveWcyNGwzYmRLRnFCTUtpRmcwMFBZQjMwZlUrQQpiZTNPSS9sZTBJK3YrK1J3SDJBVjBCTXErVDZQY0FHakNDMVExWkIwd1A5L1ZxTk1XcTVsYks5d0Q0NklRaVNpCitTZ0lRZUU3SG9pQVpYckdPMFk3bDlQMytWUm9YalJRYnFmbjNFVE5MOVp2UXVhcndBWUM5SXg1TXhVclM1YWcKT21mamM4YmZrcFlERkFYUlhtZEtOSVNKbXRDZWJYMmtEcnBQOEJkYXN4N0Z6c3g1OWNFVUhDbDJhSk9XWGM3Ugo1bTNqdU9WTDFIVXhqUUlEQVFBQm95QXdIakFjQmdOVkhSRUVGVEFUZ2hGbllYUmxkMkY1S2k1amJXbDRMbkpwCmNEQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUFNdTN4b2MyTFcyVUV4QUFJWVlXRUVUZ2dMTnJsR29ueHRlU3UKanVKak9SK2lrNVNWTG4wbEV1MjIreitGQ0E3Z1NrOUZrV3UrdjlxbmZPZm0yQW0rV0tZV3YzZEo1UnlwVy9oRApOWGtPWXhWSk5ZRnhlU2huSG9oTnFxNGVES3BkcVN4RWN1RXJGWEpkTGJaUDF1TnM0V0lPS25UaGd6aGtwdXk3CnRaUm9zdk9GMVg1dUwxZnJWSnpITjVqQVNFREFhN2hKTm1RMjRraCtkcy9HZTM5ZkdEOHBLMzFDV2huSVhlRG8KdktEN3dpdmkvZ1NPQnRjUldXTHZVOFNpelprUzNoZ1R3MGxTT2Y1Z2V1enZhc0NFWWxxcktGc3NqNmNUemJDQgp4eTNyYTNXYXpSVE5UVzRUbWtIbENVQzlJM29XVFR4dzVpUXhGL0kya1FRbndSN0wzdz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS01Mi40MS44MC4xMDQtLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJRGdUQ0NBbW1nQXdJQkFnSUpBS0xkWjhVaWdJQWVNQTBHQ1NxR1NJYjNEUUVCQlFVQU1HOHhDekFKQmdOVgpCQVlUQWxWVE1STXdFUVlEVlFRSURBcERZV3hwWm05eWJtbGhNUkl3RUFZRFZRUUhEQWxEYkdGeVpXMXZiblF4Ckd6QVpCZ05WQkFvTUVsQnlhWFpoZEdWbmNtbDBlU0JEYjNKd0xqRWFNQmdHQTFVRUF3d1JaMkYwWlhkaGVTb3UKWTIxcGVDNXlhWEF3SGhjTk1Ua3dNekExTVRnek5UVTBXaGNOTWprd016QXlNVGd6TlRVMFdqQnZNUXN3Q1FZRApWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTJ4aGNtVnRiMjUwCk1Sc3dHUVlEVlFRS0RCSlFjbWwyWVhSbFozSnBkSGtnUTI5eWNDNHhHakFZQmdOVkJBTU1FV2RoZEdWM1lYa3EKTG1OdGFYZ3VjbWx3TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUE5K0FheHdEUAp4SGJoTG1uNEhvWnUwb1VNNDhRdWZjNlQ1WEVaVHJwTXJxSkFvdVhrKzYxSmMwRUZIOTYvc2JqN1Z5dm5YUFJvCmdJRU5iazJZODRCa0I5U2tSTUlYeWEvZ2g5ZE9FRFNnbnZqL3lnMjRsM2JkS0ZxQk1LaUZnMDBQWUIzMGZVK0EKYmUzT0kvbGUwSSt2KytSd0gyQVYwQk1xK1Q2UGNBR2pDQzFRMVpCMHdQOS9WcU5NV3E1bGJLOXdENDZJUWlTaQorU2dJUWVFN0hvaUFaWHJHTzBZN2w5UDMrVlJvWGpSUWJxZm4zRVROTDladlF1YXJ3QVlDOUl4NU14VXJTNWFnCk9tZmpjOGJma3BZREZBWFJYbWRLTklTSm10Q2ViWDJrRHJwUDhCZGFzeDdGenN4NTljRVVIQ2wyYUpPV1hjN1IKNW0zanVPVkwxSFV4alFJREFRQUJveUF3SGpBY0JnTlZIUkVFRlRBVGdoRm5ZWFJsZDJGNUtpNWpiV2w0TG5KcApjREFOQmdrcWhraUc5dzBCQVFVRkFBT0NBUUVBTXUzeG9jMkxXMlVFeEFBSVlZV0VFVGdnTE5ybEdvbnh0ZVN1Cmp1SmpPUitpazVTVkxuMGxFdTIyK3orRkNBN2dTazlGa1d1K3Y5cW5mT2ZtMkFtK1dLWVd2M2RKNVJ5cFcvaEQKTlhrT1l4VkpOWUZ4ZVNobkhvaE5xcTRlREtwZHFTeEVjdUVyRlhKZExiWlAxdU5zNFdJT0tuVGhnemhrcHV5Nwp0WlJvc3ZPRjFYNXVMMWZyVkp6SE41akFTRURBYTdoSk5tUTI0a2grZHMvR2UzOWZHRDhwSzMxQ1dobklYZURvCnZLRDd3aXZpL2dTT0J0Y1JXV0x2VThTaXpaa1MzaGdUdzBsU09mNWdldXp2YXNDRVlscXJLRnNzajZjVHpiQ0IKeHkzcmEzV2F6UlROVFc0VG1rSGxDVUM5STNvV1RUeHc1aVF4Ri9JMmtRUW53UjdMM3c9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMTguMjM3LjE0Ny4xMDUtLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJRGJEQ0NBbFNnQXdJQkFnSUpBT1VOdFpuZUlZRUNNQTBHQ1NxR1NJYjNEUUVCQlFVQU1HZ3hDekFKQmdOVgpCQVlUQWxWVE1STXdFUVlEVlFRSURBcERZV3hwWm05eWJtbGhNUkl3RUFZRFZRUUhEQWxEYkdGeVpXMXZiblF4Ckd6QVpCZ05WQkFvTUVsQnlhWFpoZEdWbmNtbDBlU0JEYjNKd0xqRVRNQkVHQTFVRUF3d0tLaTVqYldsNExuSnAKY0RBZUZ3MHhPVEF6TURVeE9ETTFORE5hRncweU9UQXpNREl4T0RNMU5ETmFNR2d4Q3pBSkJnTlZCQVlUQWxWVApNUk13RVFZRFZRUUlEQXBEWVd4cFptOXlibWxoTVJJd0VBWURWUVFIREFsRGJHRnlaVzF2Ym5ReEd6QVpCZ05WCkJBb01FbEJ5YVhaaGRHVm5jbWwwZVNCRGIzSndMakVUTUJFR0ExVUVBd3dLS2k1amJXbDRMbkpwY0RDQ0FTSXcKRFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQVBQMFd5VmtmWkEvQ0VkMkRnS3BjdWRuMG9EaApEd3NqbXg4TEJEV3NVZ1F6eUxyRmlWaWdmVW1VZWZrblVIM2RUSmptaUp0R3FMc2F5Q25XZHFXTEhQSll2RmZzCldZVzBJR0Y5M1VHLzRONVVBV080b2tDM0NZZ0tTaTRla3BmdzJ6Z1pxMGdtYnpUblhjSEY5Z2ZtUTdqSlVLU0UKdEpQU056WHErUFplSlRDOXpKQWI0TGo4UXpIMThyRE04RGFMMnkxbnMwWTJIdTBlZEJGbi9PcWF2QkpLYi91QQptM0FFanFlT2hDN0VRVWpWYW1XbFRCUHQ0MCtCLzZhRkpYNUJZbTJKRmtSc0dCSXlCVkw0Nk12QzAyTWd6VFQ5CmJKSUpmd3FtQmFUcnV3ZW1OZ3pHdTdKazAzaHFxUzFUVUV2U0k2L3g4YlZvYmEzb3JjS2tmOUhzRGpFQ0F3RUEKQWFNWk1CY3dGUVlEVlIwUkJBNHdESUlLS2k1amJXbDRMbkpwY0RBTkJna3Foa2lHOXcwQkFRVUZBQU9DQVFFQQpuZVVvY040QWJjUUFDMStiM1RvOHU1VUdkYUd4aGNHeVpCbEFvZW5SVmRqWEszbFRqc01kTVdiNFFjdGdOZklmClUvenVVbjJteFRtRi9la1AwZ0NDZ3RsZVpyOStEWUtVNWhsWGs4SzEwdUt4R0Q2RXZvaVhaemxmZVV1b3RncDIKcXZJM3lzT20vaHZDZnlFa3FoZkh0YnhqVjdqN3Y3ZVFGUGJ2TmFYYkxhMHlyNEM0dk1LL1owOVVpOUpyWi9aNApjeUlreGZDNi9yT3FBaXJTZElwMDlFR2l3N0dNOGd1SHlnZ0U0SWlackRzbFQ4VjN4SWw5ODVjYkN4U3hlVzFSCnRnSDRyZEVYdVZlOSszMW9KaG1YT0U5dXgyakNvcDl0RUpNZ1dnN0hTdHJKNXBsUGJiK0htam9YM25CTzA0RTUKNm01MlB5ek1OVisyTjIxSVBwcEt3QT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1Mi4xMS4xMzYuMjM4LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURiRENDQWxTZ0F3SUJBZ0lKQU9VTnRabmVJWUVDTUEwR0NTcUdTSWIzRFFFQkJRVUFNR2d4Q3pBSkJnTlYKQkFZVEFsVlRNUk13RVFZRFZRUUlEQXBEWVd4cFptOXlibWxoTVJJd0VBWURWUVFIREFsRGJHRnlaVzF2Ym5ReApHekFaQmdOVkJBb01FbEJ5YVhaaGRHVm5jbWwwZVNCRGIzSndMakVUTUJFR0ExVUVBd3dLS2k1amJXbDRMbkpwCmNEQWVGdzB4T1RBek1EVXhPRE0xTkROYUZ3MHlPVEF6TURJeE9ETTFORE5hTUdneEN6QUpCZ05WQkFZVEFsVlQKTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbERiR0Z5WlcxdmJuUXhHekFaQmdOVgpCQW9NRWxCeWFYWmhkR1ZuY21sMGVTQkRiM0p3TGpFVE1CRUdBMVVFQXd3S0tpNWpiV2w0TG5KcGNEQ0NBU0l3CkRRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFQUDBXeVZrZlpBL0NFZDJEZ0twY3VkbjBvRGgKRHdzam14OExCRFdzVWdRenlMckZpVmlnZlVtVWVma25VSDNkVEpqbWlKdEdxTHNheUNuV2RxV0xIUEpZdkZmcwpXWVcwSUdGOTNVRy80TjVVQVdPNG9rQzNDWWdLU2k0ZWtwZncyemdacTBnbWJ6VG5YY0hGOWdmbVE3akpVS1NFCnRKUFNOelhxK1BaZUpUQzl6SkFiNExqOFF6SDE4ckRNOERhTDJ5MW5zMFkySHUwZWRCRm4vT3FhdkJKS2IvdUEKbTNBRWpxZU9oQzdFUVVqVmFtV2xUQlB0NDArQi82YUZKWDVCWW0ySkZrUnNHQkl5QlZMNDZNdkMwMk1nelRUOQpiSklKZndxbUJhVHJ1d2VtTmd6R3U3SmswM2hxcVMxVFVFdlNJNi94OGJWb2JhM29yY0trZjlIc0RqRUNBd0VBCkFhTVpNQmN3RlFZRFZSMFJCQTR3RElJS0tpNWpiV2w0TG5KcGNEQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUEKbmVVb2NONEFiY1FBQzErYjNUbzh1NVVHZGFHeGhjR3laQmxBb2VuUlZkalhLM2xUanNNZE1XYjRRY3RnTmZJZgpVL3p1VW4ybXhUbUYvZWtQMGdDQ2d0bGVacjkrRFlLVTVobFhrOEsxMHVLeEdENkV2b2lYWnpsZmVVdW90Z3AyCnF2STN5c09tL2h2Q2Z5RWtxaGZIdGJ4alY3ajd2N2VRRlBidk5hWGJMYTB5cjRDNHZNSy9aMDlVaTlKclovWjQKY3lJa3hmQzYvck9xQWlyU2RJcDA5RUdpdzdHTThndUh5Z2dFNElpWnJEc2xUOFYzeElsOTg1Y2JDeFN4ZVcxUgp0Z0g0cmRFWHVWZTkrMzFvSmhtWE9FOXV4MmpDb3A5dEVKTWdXZzdIU3RySjVwbFBiYitIbWpvWDNuQk8wNEU1CjZtNTJQeXpNTlYrMk4yMUlQcHBLd0E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMzQuMjEzLjc5LjMxLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURiRENDQWxTZ0F3SUJBZ0lKQU9VTnRabmVJWUVDTUEwR0NTcUdTSWIzRFFFQkJRVUFNR2d4Q3pBSkJnTlYKQkFZVEFsVlRNUk13RVFZRFZRUUlEQXBEWVd4cFptOXlibWxoTVJJd0VBWURWUVFIREFsRGJHRnlaVzF2Ym5ReApHekFaQmdOVkJBb01FbEJ5YVhaaGRHVm5jbWwwZVNCRGIzSndMakVUTUJFR0ExVUVBd3dLS2k1amJXbDRMbkpwCmNEQWVGdzB4T1RBek1EVXhPRE0xTkROYUZ3MHlPVEF6TURJeE9ETTFORE5hTUdneEN6QUpCZ05WQkFZVEFsVlQKTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbERiR0Z5WlcxdmJuUXhHekFaQmdOVgpCQW9NRWxCeWFYWmhkR1ZuY21sMGVTQkRiM0p3TGpFVE1CRUdBMVVFQXd3S0tpNWpiV2w0TG5KcGNEQ0NBU0l3CkRRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFQUDBXeVZrZlpBL0NFZDJEZ0twY3VkbjBvRGgKRHdzam14OExCRFdzVWdRenlMckZpVmlnZlVtVWVma25VSDNkVEpqbWlKdEdxTHNheUNuV2RxV0xIUEpZdkZmcwpXWVcwSUdGOTNVRy80TjVVQVdPNG9rQzNDWWdLU2k0ZWtwZncyemdacTBnbWJ6VG5YY0hGOWdmbVE3akpVS1NFCnRKUFNOelhxK1BaZUpUQzl6SkFiNExqOFF6SDE4ckRNOERhTDJ5MW5zMFkySHUwZWRCRm4vT3FhdkJKS2IvdUEKbTNBRWpxZU9oQzdFUVVqVmFtV2xUQlB0NDArQi82YUZKWDVCWW0ySkZrUnNHQkl5QlZMNDZNdkMwMk1nelRUOQpiSklKZndxbUJhVHJ1d2VtTmd6R3U3SmswM2hxcVMxVFVFdlNJNi94OGJWb2JhM29yY0trZjlIc0RqRUNBd0VBCkFhTVpNQmN3RlFZRFZSMFJCQTR3RElJS0tpNWpiV2w0TG5KcGNEQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUEKbmVVb2NONEFiY1FBQzErYjNUbzh1NVVHZGFHeGhjR3laQmxBb2VuUlZkalhLM2xUanNNZE1XYjRRY3RnTmZJZgpVL3p1VW4ybXhUbUYvZWtQMGdDQ2d0bGVacjkrRFlLVTVobFhrOEsxMHVLeEdENkV2b2lYWnpsZmVVdW90Z3AyCnF2STN5c09tL2h2Q2Z5RWtxaGZIdGJ4alY3ajd2N2VRRlBidk5hWGJMYTB5cjRDNHZNSy9aMDlVaTlKclovWjQKY3lJa3hmQzYvck9xQWlyU2RJcDA5RUdpdzdHTThndUh5Z2dFNElpWnJEc2xUOFYzeElsOTg1Y2JDeFN4ZVcxUgp0Z0g0cmRFWHVWZTkrMzFvSmhtWE9FOXV4MmpDb3A5dEVKTWdXZzdIU3RySjVwbFBiYitIbWpvWDNuQk8wNEU1CjZtNTJQeXpNTlYrMk4yMUlQcHBLd0E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tcmVnaXN0cmF0aW9uLmRlZmF1bHQuY21peC5yaXAtLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJRGtEQ0NBbmlnQXdJQkFnSUpBSm5qb3N1U3NQN2dNQTBHQ1NxR1NJYjNEUUVCQlFVQU1IUXhDekFKQmdOVgpCQVlUQWxWVE1STXdFUVlEVlFRSURBcERZV3hwWm05eWJtbGhNUkl3RUFZRFZRUUhEQWxEYkdGeVpXMXZiblF4Ckd6QVpCZ05WQkFvTUVsQnlhWFpoZEdWbmNtbDBlU0JEYjNKd0xqRWZNQjBHQTFVRUF3d1djbVZuYVhOMGNtRjAKYVc5dUtpNWpiV2w0TG5KcGNEQWVGdzB4T1RBek1EVXlNVFE1TlRaYUZ3MHlPVEF6TURJeU1UUTVOVFphTUhReApDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbERiR0Z5ClpXMXZiblF4R3pBWkJnTlZCQW9NRWxCeWFYWmhkR1ZuY21sMGVTQkRiM0p3TGpFZk1CMEdBMVVFQXd3V2NtVm4KYVhOMGNtRjBhVzl1S2k1amJXbDRMbkpwY0RDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQwpnZ0VCQU9RS3ZxamRoMzVvK01FQ0JoQ3dvcEp6UGxRTm1xMmlQYmV3Uk50STAyYlVOSzNrTFFVYkZsWWR6TkdaClM0R1lYR2M1TytqZGk4U2x4ODJyMWtkano1UFBDTkZCQVJJc09QL0w4cjNER2VXK3llSmRnQlpqbTFzM3lsa2EKbXQ0QWppcS9iTmp5c1M2TC9XU09wK3NWdW1EeHRCRXpPL1VUVTFPNlFSbnpVcGhMYWlXRU5tRXJHdnNIMENaVgpxMzhJYTU4ay9RakNBenBVY1lpNGoybDFmYjA3eHFGY1FEOEg2U21VTTI5N1V5UW9zRHJwOHVrZElvMzFLb3hyCjRYRG5uTk5zWVN0QzI2dHpITWVLdUoyV2wrM1l6c1N5ZmxmTTJZRWNLRTMxc3FCOURTMzZVa0o4Sjg0ZUxzSE4KSW1HZzNXb2RGQXZpREI2NytqWERiQjMwTmtNQ0F3RUFBYU1sTUNNd0lRWURWUjBSQkJvd0dJSVdjbVZuYVhOMApjbUYwYVc5dUtpNWpiV2w0TG5KcGNEQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUFGOW1OemsrZytvNjI2UmxsCnQzZjMvMXFJeVlRcllKMEJqU1dDS1lFRk1DZ1o0SmliQUpqQXZJYWpoVllFUnRsdGZmTStZS2NkRTJrVHBkekoKMFlKdVVuUmZ1djZzVm5YbFZWdWdVVW5kNElPaWdtamJDZE0zMmsxNzBDWU1tMGFpd0d4bDRGck5hOGVpN0FJYQp4L3MxbitzcVdxM0hlVzVMWGpub1ZiK3MzSGVDV0l1TGZjZ3J1cmZ5ZThGbk5oeTE0SEZ6eFZZWWVmSUttMFhMCitEUGxjR0dHbS9QUFl0M3U0YTIrclAzeGFpaGM2NWRUYTB1NXRmL1hQWHRQeFREUEZqMkplUURGeG83UVJSRWIKUEQ4OUN0WW53dVA5MzdDcmt2Q0tyTDBHa1cxRlZpWEtxWlk5RjV1aHhydkxJcHpoYk5ycy9FYnR3ZVkzNVhHTApEQ0NNa2c9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwdWJrZXkxLjIuMy40OjEyMzQBAgNGRkZGRkZGRkZGRkZGRkZGQzkwRkRBQTIyMTY4QzIzNEM0QzY2MjhCODBEQzFDRDEyOTAyNEUwODhBNjdDQzc0MDIwQkJFQTYzQjEzOUIyMjUxNEEwODc5OEUzNDA0RERFRjk1MTlCM0NEM0E0MzFCMzAyQjBBNkRGMjVGMTQzNzRGRTEzNTZENkQ1MUMyNDVFNDg1QjU3NjYyNUU3RUM2RjQ0QzQyRTlBNjM3RUQ2QjBCRkY1Q0I2RjQwNkI3RURFRTM4NkJGQjVBODk5RkE1QUU5RjI0MTE3QzRCMUZFNjQ5Mjg2NjUxRUNFNDVCM0RDMjAwN0NCOEExNjNCRjA1OThEQTQ4MzYxQzU1RDM5QTY5MTYzRkE4RkQyNENGNUY4MzY1NUQyM0RDQTNBRDk2MUM2MkYzNTYyMDg1NTJCQjlFRDUyOTA3NzA5Njk2NkQ2NzBDMzU0RTRBQkM5ODA0RjE3NDZDMDhDQTE4MjE3QzMyOTA1RTQ2MkUzNkNFM0JFMzlFNzcyQzE4MEU4NjAzOUIyNzgzQTJFQzA3QTI4RkI1QzU1REYwNkY0QzUyQzlERTJCQ0JGNjk1NTgxNzE4Mzk5NTQ5N0NFQTk1NkFFNTE1RDIyNjE4OThGQTA1MTAxNTcyOEU1QThBQUNBQTY4RkZGRkZGRkZGRkZGRkZGRjAyN0ZGRkZGRkZGRkZGRkZGRkU0ODdFRDUxMTBCNDYxMUE2MjYzMzE0NUMwNkUwRTY4OTQ4MTI3MDQ0NTMzRTYzQTAxMDVERjUzMUQ4OUNEOTEyOEE1MDQzQ0M3MUEwMjZFRjdDQThDRDlFNjlEMjE4RDk4MTU4NTM2RjkyRjhBMUJBN0YwOUFCNkI2QThFMTIyRjI0MkRBQkIzMTJGM0Y2MzdBMjYyMTc0RDMxQkY2QjU4NUZGQUU1QjdBMDM1QkY2RjcxQzM1RkRBRDQ0Q0ZEMkQ3NEY5MjA4QkUyNThGRjMyNDk0MzMyOEY2NzIyRDlFRTEwMDNFNUM1MEIxREY4MkNDNkQyNDFCMEUyQUU5Q0QzNDhCMUZENDdFOTI2N0FGQzFCMkFFOTFFRTUxRDZDQjBFMzE3OUFCMTA0MkE5NURDRjZBOTQ4M0I4NEI0QjM2QjM4NjFBQTcyNTVFNEMwMjc4QkEzNjA0NjUwQzEwQkUxOTQ4MkYyMzE3MUI2NzFERjFDRjNCOTYwQzA3NDMwMUNEOTNDMUQxNzYwM0QxNDdEQUUyQUVGODM3QTYyOTY0RUYxNUU1RkI0QUFDMEI4QzFDQ0FBNEJFNzU0QUI1NzI4QUU5MTMwQzRDN0QwMjg4MEFCOTQ3MkQ0NTU2NTUzNDdGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQzkwRkRBQTIyMTY4QzIzNEM0QzY2MjhCODBEQzFDRDEyOTAyNEUwODhBNjdDQzc0MDIwQkJFQTYzQjEzOUIyMjUxNEEwODc5OEUzNDA0RERFRjk1MTlCM0NEM0E0MzFCMzAyQjBBNkRGMjVGMTQzNzRGRTEzNTZENkQ1MUMyNDVFNDg1QjU3NjYyNUU3RUM2RjQ0QzQyRTlBNjM3RUQ2QjBCRkY1Q0I2RjQwNkI3RURFRTM4NkJGQjVBODk5RkE1QUU5RjI0MTE3QzRCMUZFNjQ5Mjg2NjUxRUNFNDVCM0RDMjAwN0NCOEExNjNCRjA1OThEQTQ4MzYxQzU1RDM5QTY5MTYzRkE4RkQyNENGNUY4MzY1NUQyM0RDQTNBRDk2MUM2MkYzNTYyMDg1NTJCQjlFRDUyOTA3NzA5Njk2NkQ2NzBDMzU0RTRBQkM5ODA0RjE3NDZDMDhDQTE4MjE3QzMyOTA1RTQ2MkUzNkNFM0JFMzlFNzcyQzE4MEU4NjAzOUIyNzgzQTJFQzA3QTI4RkI1QzU1REYwNkY0QzUyQzlERTJCQ0JGNjk1NTgxNzE4Mzk5NTQ5N0NFQTk1NkFFNTE1RDIyNjE4OThGQTA1MTAxNTcyOEU1QThBQUNBQTY4RkZGRkZGRkZGRkZGRkZGRjAyN0ZGRkZGRkZGRkZGRkZGRkU0ODdFRDUxMTBCNDYxMUE2MjYzMzE0NUMwNkUwRTY4OTQ4MTI3MDQ0NTMzRTYzQTAxMDVERjUzMUQ4OUNEOTEyOEE1MDQzQ0M3MUEwMjZFRjdDQThDRDlFNjlEMjE4RDk4MTU4NTM2RjkyRjhBMUJBN0YwOUFCNkI2QThFMTIyRjI0MkRBQkIzMTJGM0Y2MzdBMjYyMTc0RDMxQkY2QjU4NUZGQUU1QjdBMDM1QkY2RjcxQzM1RkRBRDQ0Q0ZEMkQ3NEY5MjA4QkUyNThGRjMyNDk0MzMyOEY2NzIyRDlFRTEwMDNFNUM1MEIxREY4MkNDNkQyNDFCMEUyQUU5Q0QzNDhCMUZENDdFOTI2N0FGQzFCMkFFOTFFRTUxRDZDQjBFMzE3OUFCMTA0MkE5NURDRjZBOTQ4M0I4NEI0QjM2QjM4NjFBQTcyNTVFNEMwMjc4QkEzNjA0NjUwQzEwQkUxOTQ4MkYyMzE3MUI2NzFERjFDRjNCOTYwQzA3NDMwMUNEOTNDMUQxNzYwM0QxNDdEQUUyQUVGODM3QTYyOTY0RUYxNUU1RkI0QUFDMEI4QzFDQ0FBNEJFNzU0QUI1NzI4QUU5MTMwQzRDN0QwMjg4MEFCOTQ3MkQ0NTU2NTUzNDdGRkZGRkZGRkZGRkZGRkY=") ) -// Test that DecodeNDF() fills all the fields with the proper data. -func TestDecodeNDF(t *testing.T) { +// Test that Unmarshal() fills all the fields with the proper data. +func TestUnmarshal(t *testing.T) { var received, expected string - jsonData, signature, err := DecodeNDF(ExampleNDF) + jsonData, err := Unmarshal([]byte(ExampleNDF)) if err != nil { - t.Errorf("DecodeNDF() unexpectedly produced an error"+ - "\n\treceived: %#v\n\texpected: %#v", - err, nil) + t.Errorf("Unmarshal() returned an error: %+v", err) } timestamp, _ := time.Parse(time.RFC3339, "2019-06-04T20:48:48-07:00") /* Ensure fields are populated with the correct data */ - // Signature - if !bytes.Equal(signature, SignatureBytes) { - t.Errorf("DecodeNDF() did not return the correct signature"+ - "\n\treceived: %v\n\texpected: %v", - signature, SignatureBytes) - } - // Timestamp + fmt.Printf("%s\n", jsonData.Timestamp) if !jsonData.Timestamp.Equal(timestamp) { - t.Errorf("DecodeNDF() did not correctly set Timestamp"+ + t.Errorf("Unmarshal() did not correctly set Timestamp"+ "\n\treceived: %#v\n\texpected: %#v", jsonData.Timestamp, timestamp) } @@ -119,13 +108,13 @@ func TestDecodeNDF(t *testing.T) { for i, val := range gateways { if jsonData.Gateways[i].Address != val.Address { - t.Errorf("DecodeNDF() did not correctly set Gateways[%d].address"+ + t.Errorf("Unmarshal() did not correctly set Gateways[%d].address"+ "\n\treceived: %#v\n\texpected: %#v", i, jsonData.Gateways[i].Address, val.Address) } if jsonData.Gateways[i].TlsCertificate != val.TlsCertificate { - t.Errorf("DecodeNDF() did not correctly set Gateways[%d].TlsCertificate"+ + t.Errorf("Unmarshal() did not correctly set Gateways[%d].TlsCertificate"+ "\n\treceived: %#v\n\texpected: %#v", i, jsonData.Gateways[i].TlsCertificate, val.TlsCertificate) } @@ -141,19 +130,19 @@ func TestDecodeNDF(t *testing.T) { for i, val := range nodes { if !reflect.DeepEqual(jsonData.Nodes[i].ID, val.ID) { - t.Errorf("DecodeNDF() did not correctly set Nodes[%d] ID"+ + t.Errorf("Unmarshal() did not correctly set Nodes[%d] ID"+ "\n\treceived: %#v\n\texpected: %#v", i, jsonData.Nodes[i].ID, val.ID) } if jsonData.Nodes[i].Address != val.Address { - t.Errorf("DecodeNDF() did not correctly set Nodes[%d].Address"+ + t.Errorf("Unmarshal() did not correctly set Nodes[%d].Address"+ "\n\treceived: %#v\n\texpected: %#v", i, jsonData.Nodes[i].Address, val.Address) } if jsonData.Nodes[i].TlsCertificate != val.TlsCertificate { - t.Errorf("DecodeNDF() did not correctly set Nodes[%d].TlsCertificate"+ + t.Errorf("Unmarshal() did not correctly set Nodes[%d].TlsCertificate"+ "\n\treceived: %#v\n\texpected: %#v", i, jsonData.Nodes[i].TlsCertificate, val.TlsCertificate) } @@ -163,7 +152,7 @@ func TestDecodeNDF(t *testing.T) { received = jsonData.Registration.Address expected = "registration.default.cmix.rip" if received != expected { - t.Errorf("DecodeNDF() did not correctly set Registration.Address"+ + t.Errorf("Unmarshal() did not correctly set Registration.Address"+ "\n\treceived: %#v\n\texpected: %#v", received, expected) } @@ -171,7 +160,7 @@ func TestDecodeNDF(t *testing.T) { received = jsonData.Registration.TlsCertificate expected = "-----BEGIN CERTIFICATE-----\nMIIDkDCCAnigAwIBAgIJAJnjosuSsP7gMA0GCSqGSIb3DQEBBQUAMHQxCzAJBgNV\nBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQx\nGzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjEfMB0GA1UEAwwWcmVnaXN0cmF0\naW9uKi5jbWl4LnJpcDAeFw0xOTAzMDUyMTQ5NTZaFw0yOTAzMDIyMTQ5NTZaMHQx\nCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFy\nZW1vbnQxGzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjEfMB0GA1UEAwwWcmVn\naXN0cmF0aW9uKi5jbWl4LnJpcDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBAOQKvqjdh35o+MECBhCwopJzPlQNmq2iPbewRNtI02bUNK3kLQUbFlYdzNGZ\nS4GYXGc5O+jdi8Slx82r1kdjz5PPCNFBARIsOP/L8r3DGeW+yeJdgBZjm1s3ylka\nmt4Ajiq/bNjysS6L/WSOp+sVumDxtBEzO/UTU1O6QRnzUphLaiWENmErGvsH0CZV\nq38Ia58k/QjCAzpUcYi4j2l1fb07xqFcQD8H6SmUM297UyQosDrp8ukdIo31Koxr\n4XDnnNNsYStC26tzHMeKuJ2Wl+3YzsSyflfM2YEcKE31sqB9DS36UkJ8J84eLsHN\nImGg3WodFAviDB67+jXDbB30NkMCAwEAAaMlMCMwIQYDVR0RBBowGIIWcmVnaXN0\ncmF0aW9uKi5jbWl4LnJpcDANBgkqhkiG9w0BAQUFAAOCAQEAF9mNzk+g+o626Rll\nt3f3/1qIyYQrYJ0BjSWCKYEFMCgZ4JibAJjAvIajhVYERtltffM+YKcdE2kTpdzJ\n0YJuUnRfuv6sVnXlVVugUUnd4IOigmjbCdM32k170CYMm0aiwGxl4FrNa8ei7AIa\nx/s1n+sqWq3HeW5LXjnoVb+s3HeCWIuLfcgrurfye8FnNhy14HFzxVYYefIKm0XL\n+DPlcGGGm/PPYt3u4a2+rP3xaihc65dTa0u5tf/XPXtPxTDPFj2JeQDFxo7QRREb\nPD89CtYnwuP937CrkvCKrL0GkW1FViXKqZY9F5uhxrvLIpzhbNrs/EbtweY35XGL\nDCCMkg==\n-----END CERTIFICATE-----" if received != expected { - t.Errorf("DecodeNDF() did not correctly set Registration.TlsCertificate"+ + t.Errorf("Unmarshal() did not correctly set Registration.TlsCertificate"+ "\n\treceived: %#v\n\texpected: %#v", received, expected) } @@ -180,7 +169,7 @@ func TestDecodeNDF(t *testing.T) { received = jsonData.Notification.Address expected = "notification.default.cmix.rip" if received != expected { - t.Errorf("DecodeNDF() did not correctly set Registration.Address"+ + t.Errorf("Unmarshal() did not correctly set Registration.Address"+ "\n\treceived: %#v\n\texpected: %#v", received, expected) } @@ -188,7 +177,7 @@ func TestDecodeNDF(t *testing.T) { received = jsonData.Notification.TlsCertificate expected = "-----BEGIN CERTIFICATE-----\nMIIDkDCCAnigAwIBAgIJAJnjosuSsP7gMA0GCSqGSIb3DQEBBQUAMHQxCzAJBgNV\nBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQx\nGzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjEfMB0GA1UEAwwWcmVnaXN0cmF0\naW9uKi5jbWl4LnJpcDAeFw0xOTAzMDUyMTQ5NTZaFw0yOTAzMDIyMTQ5NTZaMHQx\nCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFy\nZW1vbnQxGzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjEfMB0GA1UEAwwWcmVn\naXN0cmF0aW9uKi5jbWl4LnJpcDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBAOQKvqjdh35o+MECBhCwopJzPlQNmq2iPbewRNtI02bUNK3kLQUbFlYdzNGZ\nS4GYXGc5O+jdi8Slx82r1kdjz5PPCNFBARIsOP/L8r3DGeW+yeJdgBZjm1s3ylka\nmt4Ajiq/bNjysS6L/WSOp+sVumDxtBEzO/UTU1O6QRnzUphLaiWENmErGvsH0CZV\nq38Ia58k/QjCAzpUcYi4j2l1fb07xqFcQD8H6SmUM297UyQosDrp8ukdIo31Koxr\n4XDnnNNsYStC26tzHMeKuJ2Wl+3YzsSyflfM2YEcKE31sqB9DS36UkJ8J84eLsHN\nImGg3WodFAviDB67+jXDbB30NkMCAwEAAaMlMCMwIQYDVR0RBBowGIIWcmVnaXN0\ncmF0aW9uKi5jbWl4LnJpcDANBgkqhkiG9w0BAQUFAAOCAQEAF9mNzk+g+o626Rll\nt3f3/1qIyYQrYJ0BjSWCKYEFMCgZ4JibAJjAvIajhVYERtltffM+YKcdE2kTpdzJ\n0YJuUnRfuv6sVnXlVVugUUnd4IOigmjbCdM32k170CYMm0aiwGxl4FrNa8ei7AIa\nx/s1n+sqWq3HeW5LXjnoVb+s3HeCWIuLfcgrurfye8FnNhy14HFzxVYYefIKm0XL\n+DPlcGGGm/PPYt3u4a2+rP3xaihc65dTa0u5tf/XPXtPxTDPFj2JeQDFxo7QRREb\nPD89CtYnwuP937CrkvCKrL0GkW1FViXKqZY9F5uhxrvLIpzhbNrs/EbtweY35XGL\nDCCMkg==\n-----END CERTIFICATE-----" if received != expected { - t.Errorf("DecodeNDF() did not correctly set Registration.TlsCertificate"+ + t.Errorf("Unmarshal() did not correctly set Registration.TlsCertificate"+ "\n\treceived: %#v\n\texpected: %#v", received, expected) } @@ -197,12 +186,24 @@ func TestDecodeNDF(t *testing.T) { expectedUdbId := make([]byte, 32) expectedUdbId[0] = 1 if !bytes.Equal(jsonData.UDB.ID, expectedUdbId) { - t.Errorf("DecodeNDF() did not correctly set jsonData.UDB.ID"+ + t.Errorf("Unmarshal() did not correctly set UDB.ID"+ "\n\treceived: %#v\n\texpected: %#v", jsonData.UDB.ID, expectedUdbId) } + + if !bytes.Equal(jsonData.UDB.DhPubKey, []byte{1, 2, 3}) { + t.Errorf("Unmarshal() did not correctly set UDB.DhPubKey"+ + "\n\treceived: %+v\n\texpected: %+v", + jsonData.UDB.DhPubKey, []byte{1, 2, 3}) + } + + if jsonData.UDB.Address != "1.2.3.4:1234" { + t.Errorf("Unmarshal() did not correctly set UDB.DhPubKey"+ + "\n\treceived: %s\n\texpected: %s", + jsonData.UDB.Address, "1.2.3.4:1234") + } if !reflect.DeepEqual(jsonData.UDB.Cert, "pubkey") { - t.Errorf("DecodeNDF() did not correctly set jsonData.UDB.Cert"+ + t.Errorf("Unmarshal() did not correctly set UDB.Cert"+ "\n\treceived: %#v\n\texpected: %#v", jsonData.UDB.Cert, "pubkey") } @@ -211,7 +212,7 @@ func TestDecodeNDF(t *testing.T) { received = jsonData.E2E.Prime expected = "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF" if received != expected { - t.Errorf("DecodeNDF() did not correctly set E2E.Prime"+ + t.Errorf("Unmarshal() did not correctly set E2E.Prime"+ "\n\treceived: %#v\n\texpected: %#v", received, expected) } @@ -219,7 +220,7 @@ func TestDecodeNDF(t *testing.T) { received = jsonData.E2E.SmallPrime expected = "7FFFFFFFFFFFFFFFE487ED5110B4611A62633145C06E0E68948127044533E63A0105DF531D89CD9128A5043CC71A026EF7CA8CD9E69D218D98158536F92F8A1BA7F09AB6B6A8E122F242DABB312F3F637A262174D31BF6B585FFAE5B7A035BF6F71C35FDAD44CFD2D74F9208BE258FF324943328F6722D9EE1003E5C50B1DF82CC6D241B0E2AE9CD348B1FD47E9267AFC1B2AE91EE51D6CB0E3179AB1042A95DCF6A9483B84B4B36B3861AA7255E4C0278BA3604650C10BE19482F23171B671DF1CF3B960C074301CD93C1D17603D147DAE2AEF837A62964EF15E5FB4AAC0B8C1CCAA4BE754AB5728AE9130C4C7D02880AB9472D455655347FFFFFFFFFFFFFFF" if received != expected { - t.Errorf("DecodeNDF() did not correctly set E2E.Small_prime"+ + t.Errorf("Unmarshal() did not correctly set E2E.Small_prime"+ "\n\treceived: %#v\n\texpected: %#v", received, expected) } @@ -227,7 +228,7 @@ func TestDecodeNDF(t *testing.T) { received = jsonData.E2E.Generator expected = "02" if received != expected { - t.Errorf("DecodeNDF() did not correctly set E2E.Generator"+ + t.Errorf("Unmarshal() did not correctly set E2E.Generator"+ "\n\treceived: %#v\n\texpected: %#v", received, expected) } @@ -236,7 +237,7 @@ func TestDecodeNDF(t *testing.T) { received = jsonData.CMIX.Prime expected = "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF" if received != expected { - t.Errorf("DecodeNDF() did not correctly set CMIX.Prime"+ + t.Errorf("Unmarshal() did not correctly set CMIX.Prime"+ "\n\treceived: %#v\n\texpected: %#v", received, expected) } @@ -244,7 +245,7 @@ func TestDecodeNDF(t *testing.T) { received = jsonData.CMIX.SmallPrime expected = "7FFFFFFFFFFFFFFFE487ED5110B4611A62633145C06E0E68948127044533E63A0105DF531D89CD9128A5043CC71A026EF7CA8CD9E69D218D98158536F92F8A1BA7F09AB6B6A8E122F242DABB312F3F637A262174D31BF6B585FFAE5B7A035BF6F71C35FDAD44CFD2D74F9208BE258FF324943328F6722D9EE1003E5C50B1DF82CC6D241B0E2AE9CD348B1FD47E9267AFC1B2AE91EE51D6CB0E3179AB1042A95DCF6A9483B84B4B36B3861AA7255E4C0278BA3604650C10BE19482F23171B671DF1CF3B960C074301CD93C1D17603D147DAE2AEF837A62964EF15E5FB4AAC0B8C1CCAA4BE754AB5728AE9130C4C7D02880AB9472D455655347FFFFFFFFFFFFFFF" if received != expected { - t.Errorf("DecodeNDF() did not correctly set CMIX.Small_prime"+ + t.Errorf("Unmarshal() did not correctly set CMIX.Small_prime"+ "\n\treceived: %#v\n\texpected: %#v", received, expected) } @@ -252,89 +253,25 @@ func TestDecodeNDF(t *testing.T) { received = jsonData.CMIX.Generator expected = "02" if received != expected { - t.Errorf("DecodeNDF() did not correctly set CMIX.Generator"+ + t.Errorf("Unmarshal() did not correctly set CMIX.Generator"+ "\n\treceived: %#v\n\texpected: %#v", received, expected) } } -// Tests that DecodeNDF() returns an error when Unmarshal() returns an error. -func TestDecodeNDF_ErrUnmarshal(t *testing.T) { - jsonData, signature, err := DecodeNDF("test" + ExampleNDF) - - if jsonData != nil { - t.Errorf("DecodeNDF() did not corectly return nil NetworkDefinition on error"+ - "\n\treceived: %#v\n\texpected: %#v", jsonData, nil) - } - - if !bytes.Equal(signature, nil) { - t.Errorf("DecodeNDF() did not corectly return the signature on error"+ - "\n\treceived: %v\n\texpected: %v", signature, nil) - } +// Tests that Unmarshal() returns an error when Unmarshal() returns an error. +func TestUnmarshal_ErrUnmarshal(t *testing.T) { + _, err := Unmarshal([]byte("{\"gateways\": 5}")) if err == nil { - t.Errorf("DecodeNDF() did not issue the expected error"+ - "\n\treceived: %#v\n\texpected: %#v", err, errors.New("")) - } -} - -// Tests that separate() throws an error when the signature is not base64. -func TestDecodeNDF_ErrDecode(t *testing.T) { - jsonData, signature, err := DecodeNDF(ExampleJSON + "\nhello") - - if jsonData != nil { - t.Errorf("DecodeNDF() did not corectly return nil NetworkDefinition on error"+ - "\n\treceived: %#v\n\texpected: %#v", jsonData, nil) - } - - if !bytes.Equal(signature, nil) { - t.Errorf("DecodeNDF() did not corectly return the signature on error"+ - "\n\treceived: %v\n\texpected: %v", signature, nil) - } - - if err == nil { - t.Errorf("separate() did not issue the expected error"+ - "\n\treceived: %#v\n\texpected: %#v", err, errors.New("")) - } -} - -// Tests that separate() correctly splits the JSON data from the string. -func TestSeparate(t *testing.T) { - jsonData, signature := separate(ExampleNDF) - - if jsonData != strings.TrimSpace(ExampleJSON) { - t.Errorf("separate() did not corectly seperate the JSON data"+ - "\n\treceived: %#v\n\texpected: %#v", - jsonData, strings.TrimSpace(ExampleJSON)) - } - - if signature != strings.TrimSpace(ExampleSignature) { - t.Errorf("DecodeNDF() did not corectly return the signature"+ - "\n\treceived: %#v\n\texpected: %#v", - signature, strings.TrimSpace(ExampleSignature)) - } -} - -// Tests that separate() returns the JSON string with an empty signature when -// the NDF file does not have at least two lines. -func TestSeparate_ErrorLines(t *testing.T) { - jsonData, signature := separate(ExampleJSON) - - if jsonData != ExampleJSON { - t.Errorf("separate() did not return the JSON string on empty signature"+ - "\n\treceived: %#v\n\texpected: %#v", jsonData, "") - } - - if signature != "" { - t.Errorf("DecodeNDF() did not corectly return an empty signature"+ - "\n\treceived: %#v\n\texpected: %#v", signature, "") + t.Errorf("Unmarshal() did not return an error for invalid JSON.") } } // Tests the consistency of Serialize(). func TestNetworkDefinition_Serialize(t *testing.T) { - ndf, _, _ := DecodeNDF(ExampleNDF) + ndf, _ := Unmarshal([]byte(ExampleNDF)) ndfBytes := ndf.Serialize() if !bytes.Equal(ndfBytes, JsonBytes) { @@ -345,7 +282,7 @@ func TestNetworkDefinition_Serialize(t *testing.T) { // Happy path func TestStripNdf(t *testing.T) { - ndf, _, err := DecodeNDF(ExampleNDF) + ndf, err := Unmarshal([]byte(ExampleNDF)) if err != nil { t.Errorf("Failed to decode ndf for testing: %+v", err) } @@ -371,9 +308,9 @@ func TestStripNdf(t *testing.T) { } func TestNetworkDefinition_Marshal(t *testing.T) { - jsonData, _, err := DecodeNDF(ExampleNDF) + jsonData, err := Unmarshal([]byte(ExampleNDF)) if err != nil { - t.Errorf("DecodeNDF() unexpectedly produced an error"+ + t.Errorf("Unmarshal() unexpectedly produced an error"+ "\n\treceived: %#v\n\texpected: %#v", err, nil) } @@ -396,9 +333,9 @@ func TestNetworkDefinition_Marshal(t *testing.T) { // Happy path: this finds an id in the hardcoded/global ExampleNDF func TestGetNodeId(t *testing.T) { - jsonData, _, err := DecodeNDF(ExampleNDF) + jsonData, err := Unmarshal([]byte(ExampleNDF)) if err != nil { - t.Errorf("DecodeNDF() unexpectedly produced an error"+ + t.Errorf("Unmarshal() unexpectedly produced an error"+ "\n\treceived: %#v\n\texpected: %#v", err, nil) } diff --git a/netTime/timeNow.go b/netTime/timeNow.go new file mode 100644 index 0000000000000000000000000000000000000000..a024cc7ffa7c9c7344cdd7497fba8d56a9a76966 --- /dev/null +++ b/netTime/timeNow.go @@ -0,0 +1,20 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +// Package netTime provides a custom time function that should provide the +// current accurate time used by the network from a custom time service. +package netTime + +import ( + "time" +) + +type NowFunc func() time.Time + +// Now returns the current accurate time. The function must be set an accurate +// time service that returns the current time with an accuracy of +/- 300 ms. +var Now NowFunc = time.Now diff --git a/netTime/timeNow_test.go b/netTime/timeNow_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6c648569cf158dcc2d32e65d75c4bbae8a9a60c7 --- /dev/null +++ b/netTime/timeNow_test.go @@ -0,0 +1,40 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package netTime + +import ( + "testing" + "time" +) + +// Happy path: tests that Now() returns time.Now() if it is unset. +func TestNow(t *testing.T) { + expectedTime := time.Now().Round(time.Millisecond) + receivedTime := Now().Round(time.Millisecond) + + if !expectedTime.Equal(receivedTime) { + t.Errorf("Returned incorrect time.\nexpected: %s\nreceived: %s", + expectedTime, receivedTime) + } +} + +// Happy path: tests that setting Now works. +func TestNow_Set(t *testing.T) { + expectedTime := time.Now() + testNow := func() time.Time { + return expectedTime + } + + Now = testNow + + now := Now() + if !Now().Equal(expectedTime) { + t.Errorf("Returned incorrect time.\nexpected: %s\nreceived: %s", + expectedTime, now) + } +}