Skip to content

Commit

Permalink
Merge branch 'master' into examples-readme
Browse files Browse the repository at this point in the history
  • Loading branch information
leohhhn authored Nov 14, 2024
2 parents febf300 + bd1d76e commit 70e1ae8
Show file tree
Hide file tree
Showing 9 changed files with 558 additions and 0 deletions.
6 changes: 6 additions & 0 deletions examples/gno.land/p/n2p5/haystack/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module gno.land/p/n2p5/haystack

require (
gno.land/p/demo/avl v0.0.0-latest
gno.land/p/n2p5/haystack/needle v0.0.0-latest
)
99 changes: 99 additions & 0 deletions examples/gno.land/p/n2p5/haystack/haystack.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package haystack

import (
"encoding/hex"
"errors"

"gno.land/p/demo/avl"
"gno.land/p/n2p5/haystack/needle"
)

var (
// ErrorNeedleNotFound is returned when a needle is not found in the haystack.
ErrorNeedleNotFound = errors.New("needle not found")
// ErrorNeedleLength is returned when a needle is not the correct length.
ErrorNeedleLength = errors.New("invalid needle length")
// ErrorHashLength is returned when a needle hash is not the correct length.
ErrorHashLength = errors.New("invalid hash length")
// ErrorDuplicateNeedle is returned when a needle already exists in the haystack.
ErrorDuplicateNeedle = errors.New("needle already exists")
// ErrorHashMismatch is returned when a needle hash does not match the needle. This should
// never happen and indicates a critical internal storage error.
ErrorHashMismatch = errors.New("storage error: hash mismatch")
// ErrorValueInvalidType is returned when a needle value is not a byte slice. This should
// never happen and indicates a critical internal storage error.
ErrorValueInvalidType = errors.New("storage error: invalid value type, expected []byte")
)

const (
// EncodedHashLength is the length of the hex-encoded needle hash.
EncodedHashLength = needle.HashLength * 2
// EncodedPayloadLength is the length of the hex-encoded needle payload.
EncodedPayloadLength = needle.PayloadLength * 2
// EncodedNeedleLength is the length of the hex-encoded needle.
EncodedNeedleLength = EncodedHashLength + EncodedPayloadLength
)

// Haystack is a permissionless, append-only, content-addressed key-value store for fix
// length messages known as needles. A needle is a 192 byte byte slice with a 32 byte
// hash (sha256) and a 160 byte payload.
type Haystack struct{ internal *avl.Tree }

// New creates a new instance of a Haystack key-value store.
func New() *Haystack {
return &Haystack{
internal: avl.NewTree(),
}
}

// Add takes a fixed-length hex-encoded needle bytes and adds it to the haystack key-value
// store. The key is the first 32 bytes of the needle hash (64 bytes hex-encoded) of the
// sha256 sum of the payload. The value is the 160 byte byte slice of the needle payload.
// An error is returned if the needle is found to be invalid.
func (h *Haystack) Add(needleHex string) error {
if len(needleHex) != EncodedNeedleLength {
return ErrorNeedleLength
}
b, err := hex.DecodeString(needleHex)
if err != nil {
return err
}
n, err := needle.FromBytes(b)
if err != nil {
return err
}
if h.internal.Has(needleHex[:EncodedHashLength]) {
return ErrorDuplicateNeedle
}
h.internal.Set(needleHex[:EncodedHashLength], n.Payload())
return nil
}

// Get takes a hex-encoded needle hash and returns the complete hex-encoded needle bytes
// and an error. Errors covers errors that span from the needle not being found, internal
// storage error inconsistencies, and invalid value types.
func (h *Haystack) Get(hash string) (string, error) {
if len(hash) != EncodedHashLength {
return "", ErrorHashLength
}
if _, err := hex.DecodeString(hash); err != nil {
return "", err
}
v, ok := h.internal.Get(hash)
if !ok {
return "", ErrorNeedleNotFound
}
b, ok := v.([]byte)
if !ok {
return "", ErrorValueInvalidType
}
n, err := needle.New(b)
if err != nil {
return "", err
}
needleHash := hex.EncodeToString(n.Hash())
if needleHash != hash {
return "", ErrorHashMismatch
}
return hex.EncodeToString(n.Bytes()), nil
}
94 changes: 94 additions & 0 deletions examples/gno.land/p/n2p5/haystack/haystack_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package haystack

import (
"encoding/hex"
"testing"

"gno.land/p/n2p5/haystack/needle"
)

func TestHaystack(t *testing.T) {
t.Parallel()

t.Run("New", func(t *testing.T) {
t.Parallel()
h := New()
if h == nil {
t.Error("New returned nil")
}
})

t.Run("Add", func(t *testing.T) {
t.Parallel()
h := New()
n, _ := needle.New(make([]byte, needle.PayloadLength))
validNeedleHex := hex.EncodeToString(n.Bytes())

testTable := []struct {
needleHex string
err error
}{
{validNeedleHex, nil},
{validNeedleHex, ErrorDuplicateNeedle},
{"bad" + validNeedleHex[3:], needle.ErrorInvalidHash},
{"XXX" + validNeedleHex[3:], hex.InvalidByteError('X')},
{validNeedleHex[:len(validNeedleHex)-2], ErrorNeedleLength},
{validNeedleHex + "00", ErrorNeedleLength},
{"000", ErrorNeedleLength},
}
for _, tt := range testTable {
err := h.Add(tt.needleHex)
if err != tt.err {
t.Error(tt.needleHex, err.Error(), "!=", tt.err.Error())
}
}
})

t.Run("Get", func(t *testing.T) {
t.Parallel()
h := New()

// genNeedleHex returns a hex-encoded needle and its hash for a given index.
genNeedleHex := func(i int) (string, string) {
b := make([]byte, needle.PayloadLength)
b[0] = byte(i)
n, _ := needle.New(b)
return hex.EncodeToString(n.Bytes()), hex.EncodeToString(n.Hash())
}

// Add a valid needle to the haystack.
validNeedleHex, validHash := genNeedleHex(0)
h.Add(validNeedleHex)

// Add a needle and break the value type.
_, brokenHashValueType := genNeedleHex(1)
h.internal.Set(brokenHashValueType, 0)

// Add a needle with invalid hash.
_, invalidHash := genNeedleHex(2)
h.internal.Set(invalidHash, make([]byte, needle.PayloadLength))

testTable := []struct {
hash string
expected string
err error
}{
{validHash, validNeedleHex, nil},
{validHash[:len(validHash)-2], "", ErrorHashLength},
{validHash + "00", "", ErrorHashLength},
{"XXX" + validHash[3:], "", hex.InvalidByteError('X')},
{"bad" + validHash[3:], "", ErrorNeedleNotFound},
{brokenHashValueType, "", ErrorValueInvalidType},
{invalidHash, "", ErrorHashMismatch},
}
for _, tt := range testTable {
actual, err := h.Get(tt.hash)
if err != tt.err {
t.Error(tt.hash, err.Error(), "!=", tt.err.Error())
}
if actual != tt.expected {
t.Error(tt.hash, actual, "!=", tt.expected)
}
}
})
}
1 change: 1 addition & 0 deletions examples/gno.land/p/n2p5/haystack/needle/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/n2p5/haystack/needle
91 changes: 91 additions & 0 deletions examples/gno.land/p/n2p5/haystack/needle/needle.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package needle

import (
"bytes"
"crypto/sha256"
"errors"
)

const (
// HashLength is the length in bytes of the hash prefix in any message
HashLength = 32
// PayloadLength is the length of the remaining bytes of the message.
PayloadLength = 160
// NeedleLength is the number of bytes required for a valid needle.
NeedleLength = HashLength + PayloadLength
)

// Needle is a container for a 160 byte payload
// and a 32 byte sha256 hash of the payload.
type Needle struct {
hash [HashLength]byte
payload [PayloadLength]byte
}

var (
// ErrorInvalidHash is an error for in invalid hash
ErrorInvalidHash = errors.New("invalid hash")
// ErrorByteSliceLength is an error for an invalid byte slice length passed in to New or FromBytes
ErrorByteSliceLength = errors.New("invalid byte slice length")
)

// New creates a Needle used for submitting a payload to a Haystack sever. It takes a Payload
// byte slice that is 160 bytes in length and returns a reference to a
// Needle and an error. The purpose of this function is to make it
// easy to create a new Needle from a payload. This function handles creating a sha256
// hash of the payload, which is used by the Needle to submit to a haystack server.
func New(p []byte) (*Needle, error) {
if len(p) != PayloadLength {
return nil, ErrorByteSliceLength
}
var n Needle
sum := sha256.Sum256(p)
copy(n.hash[:], sum[:])
copy(n.payload[:], p)
return &n, nil
}

// FromBytes is intended convert raw bytes (from UDP or storage) into a Needle.
// It takes a byte slice and expects it to be exactly the length of NeedleLength.
// The byte slice should consist of the first 32 bytes being the sha256 hash of the
// payload and the payload bytes. This function verifies the length of the byte slice,
// copies the bytes into a private [192]byte array, and validates the Needle. It returns
// a reference to a Needle and an error.
func FromBytes(b []byte) (*Needle, error) {
if len(b) != NeedleLength {
return nil, ErrorByteSliceLength
}
var n Needle
copy(n.hash[:], b[:HashLength])
copy(n.payload[:], b[HashLength:])
if err := n.validate(); err != nil {
return nil, err
}
return &n, nil
}

// Hash returns a copy of the bytes of the sha256 256 hash of the Needle payload.
func (n *Needle) Hash() []byte {
return n.Bytes()[:HashLength]
}

// Payload returns a byte slice of the Needle payload
func (n *Needle) Payload() []byte {
return n.Bytes()[HashLength:]
}

// Bytes returns a byte slice of the entire 192 byte hash + payload
func (n *Needle) Bytes() []byte {
b := make([]byte, NeedleLength)
copy(b, n.hash[:])
copy(b[HashLength:], n.payload[:])
return b
}

// validate checks that a Needle has a valid hash, it returns either nil or an error.
func (n *Needle) validate() error {
if hash := sha256.Sum256(n.Payload()); !bytes.Equal(n.Hash(), hash[:]) {
return ErrorInvalidHash
}
return nil
}
Loading

0 comments on commit 70e1ae8

Please sign in to comment.