-
Notifications
You must be signed in to change notification settings - Fork 375
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into fix/improve-nil-not-assignable
- Loading branch information
Showing
9 changed files
with
558 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module gno.land/p/n2p5/haystack/needle |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.