feat(chain): remove channels, add social feed (Phase A of v2.0.0)
Replaces the channel/membership model with a VK/Twitter-style feed:
public posts, follow graph, likes. Views are deliberately off-chain
(counted by the hosting relay, Phase B).
Removed
- EventCreateChannel, EventAddMember
- CreateChannelPayload, AddMemberPayload, ChannelMember
- prefixChannel, prefixChanMember
- chain.Channel(), chain.ChannelMembers()
- node/api_channels.go
- GetChannel, GetChannelMembers on ExplorerQuery
Added
- Events: CREATE_POST, DELETE_POST, FOLLOW, UNFOLLOW, LIKE_POST, UNLIKE_POST
- Payloads: CreatePostPayload, DeletePostPayload, FollowPayload,
UnfollowPayload, LikePostPayload, UnlikePostPayload
- Stored shape: PostRecord (author, size, hash, hosting relay, timestamp,
reply/quote refs, soft-delete flag, fee paid)
- State prefixes: post:, postbyauthor:, follow:, followin:, like:, likecount:
- Queries: Post(), PostsByAuthor(), Following(), Followers(),
LikeCount(), HasLiked()
- Cached like counter via bumpLikeCount helper
Pricing
- BasePostFee = 1000 µT (aligned with MinFee block-validation floor)
- PostByteFee = 1 µT/byte of compressed content
- Total fee credited in full to HostingRelay pub (storage compensation)
- MaxPostSize = 256 KiB
Integrity
- CREATE_POST validates content_hash length (32 B) and size range
- DELETE_POST restricted to post.Author
- Duplicate FOLLOW / LIKE rejected
- reply_to and quote_of mutually exclusive
Tests
- TestFeedCreatePost: post stored, indexed, host credited
- TestFeedInsufficientFee: underpaid post is skipped
- TestFeedFollowUnfollow: follow graph round-trips via forward + inbound indices
- TestFeedLikeUnlike: like toggles with dedup, counter stays accurate
- TestFeedDeletePostByOther: non-author deletion rejected
This is Phase A (chain-layer). Phase B adds the relay feed-mailbox
(post bodies + gossipsub) and HTTP endpoints. Phase C adds the client
Feed tab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,8 +40,14 @@ const (
|
||||
prefixHeight = "height" // height → uint64
|
||||
prefixBalance = "balance:" // balance:<pubkey> → uint64
|
||||
prefixIdentity = "id:" // id:<pubkey> → RegisterKeyPayload JSON
|
||||
prefixChannel = "chan:" // chan:<channelID> → CreateChannelPayload JSON
|
||||
prefixChanMember = "chan-member:" // chan-member:<channelID>:<memberPubKey> → "" (presence = member)
|
||||
|
||||
// Social feed (v2.0.0). Replaced the old channel keys (chan:, chan-member:).
|
||||
prefixPost = "post:" // post:<postID> → PostRecord JSON
|
||||
prefixPostByAuthor = "postbyauthor:" // postbyauthor:<author>:<ts_20d>:<postID> → "" (chrono index)
|
||||
prefixFollow = "follow:" // follow:<follower>:<target> → "" (presence = follows)
|
||||
prefixFollowInbound = "followin:" // followin:<target>:<follower> → "" (reverse index — counts followers)
|
||||
prefixLike = "like:" // like:<postID>:<liker> → "" (presence = liked)
|
||||
prefixLikeCount = "likecount:" // likecount:<postID> → uint64 (cached count)
|
||||
prefixWalletBind = "walletbind:" // walletbind:<node_pubkey> → wallet_pubkey (string)
|
||||
prefixReputation = "rep:" // rep:<pubkey> → RepStats JSON
|
||||
prefixPayChan = "paychan:" // paychan:<channelID> → PayChanState JSON
|
||||
@@ -538,11 +544,13 @@ func (c *Chain) Identity(pubKeyHex string) (*RegisterKeyPayload, error) {
|
||||
return &p, err
|
||||
}
|
||||
|
||||
// Channel returns the CreateChannelPayload for a channel ID, or nil.
|
||||
func (c *Chain) Channel(channelID string) (*CreateChannelPayload, error) {
|
||||
var p CreateChannelPayload
|
||||
// ── Feed queries (v2.0.0) ──────────────────────────────────────────────────
|
||||
|
||||
// Post returns the PostRecord for a post ID, or nil if not found.
|
||||
func (c *Chain) Post(postID string) (*PostRecord, error) {
|
||||
var p PostRecord
|
||||
err := c.db.View(func(txn *badger.Txn) error {
|
||||
item, err := txn.Get([]byte(prefixChannel + channelID))
|
||||
item, err := txn.Get([]byte(prefixPost + postID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -553,13 +561,62 @@ func (c *Chain) Channel(channelID string) (*CreateChannelPayload, error) {
|
||||
if errors.Is(err, badger.ErrKeyNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return &p, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// ChannelMembers returns the public keys of all members added to channelID.
|
||||
func (c *Chain) ChannelMembers(channelID string) ([]string, error) {
|
||||
prefix := []byte(fmt.Sprintf("%s%s:", prefixChanMember, channelID))
|
||||
var members []string
|
||||
// PostsByAuthor returns the last `limit` posts by the given author, newest
|
||||
// first. Iterates `postbyauthor:<author>:...` in reverse order. If limit
|
||||
// ≤ 0, defaults to 50; capped at 200.
|
||||
func (c *Chain) PostsByAuthor(authorPub string, limit int) ([]*PostRecord, error) {
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
if limit > 200 {
|
||||
limit = 200
|
||||
}
|
||||
prefix := []byte(prefixPostByAuthor + authorPub + ":")
|
||||
var out []*PostRecord
|
||||
|
||||
err := c.db.View(func(txn *badger.Txn) error {
|
||||
opts := badger.DefaultIteratorOptions
|
||||
opts.Prefix = prefix
|
||||
opts.Reverse = true // newest (higher ts) first — reverse iteration
|
||||
opts.PrefetchValues = false
|
||||
// For reverse iteration Badger requires seeking past the prefix range.
|
||||
seek := append([]byte{}, prefix...)
|
||||
seek = append(seek, 0xff)
|
||||
|
||||
it := txn.NewIterator(opts)
|
||||
defer it.Close()
|
||||
for it.Seek(seek); it.ValidForPrefix(prefix) && len(out) < limit; it.Next() {
|
||||
key := string(it.Item().Key())
|
||||
// key = "postbyauthor:<author>:<ts_20d>:<postID>"
|
||||
parts := strings.Split(key, ":")
|
||||
if len(parts) < 4 {
|
||||
continue
|
||||
}
|
||||
postID := parts[len(parts)-1]
|
||||
rec, err := c.postInTxn(txn, postID)
|
||||
if err != nil || rec == nil {
|
||||
continue
|
||||
}
|
||||
if rec.Deleted {
|
||||
continue
|
||||
}
|
||||
out = append(out, rec)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
// Following returns the Ed25519 pubkeys that `follower` subscribes to.
|
||||
func (c *Chain) Following(followerPub string) ([]string, error) {
|
||||
prefix := []byte(prefixFollow + followerPub + ":")
|
||||
var out []string
|
||||
err := c.db.View(func(txn *badger.Txn) error {
|
||||
opts := badger.DefaultIteratorOptions
|
||||
opts.PrefetchValues = false
|
||||
@@ -568,15 +625,95 @@ func (c *Chain) ChannelMembers(channelID string) ([]string, error) {
|
||||
defer it.Close()
|
||||
for it.Rewind(); it.Valid(); it.Next() {
|
||||
key := string(it.Item().Key())
|
||||
// key = "chan-member:<channelID>:<memberPubKey>"
|
||||
// key = "follow:<follower>:<target>"
|
||||
parts := strings.SplitN(key, ":", 3)
|
||||
if len(parts) == 3 {
|
||||
members = append(members, parts[2])
|
||||
out = append(out, parts[2])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return members, err
|
||||
return out, err
|
||||
}
|
||||
|
||||
// Followers returns the Ed25519 pubkeys that follow `target`.
|
||||
func (c *Chain) Followers(targetPub string) ([]string, error) {
|
||||
prefix := []byte(prefixFollowInbound + targetPub + ":")
|
||||
var out []string
|
||||
err := c.db.View(func(txn *badger.Txn) error {
|
||||
opts := badger.DefaultIteratorOptions
|
||||
opts.PrefetchValues = false
|
||||
opts.Prefix = prefix
|
||||
it := txn.NewIterator(opts)
|
||||
defer it.Close()
|
||||
for it.Rewind(); it.Valid(); it.Next() {
|
||||
key := string(it.Item().Key())
|
||||
parts := strings.SplitN(key, ":", 3)
|
||||
if len(parts) == 3 {
|
||||
out = append(out, parts[2])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
// LikeCount returns the cached count of likes for a post (O(1)).
|
||||
func (c *Chain) LikeCount(postID string) (uint64, error) {
|
||||
var count uint64
|
||||
err := c.db.View(func(txn *badger.Txn) error {
|
||||
item, err := txn.Get([]byte(prefixLikeCount + postID))
|
||||
if errors.Is(err, badger.ErrKeyNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return item.Value(func(val []byte) error {
|
||||
if len(val) == 8 {
|
||||
count = binary.BigEndian.Uint64(val)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
return count, err
|
||||
}
|
||||
|
||||
// HasLiked reports whether `liker` has liked the given post.
|
||||
func (c *Chain) HasLiked(postID, likerPub string) (bool, error) {
|
||||
key := []byte(prefixLike + postID + ":" + likerPub)
|
||||
var ok bool
|
||||
err := c.db.View(func(txn *badger.Txn) error {
|
||||
_, err := txn.Get(key)
|
||||
if err == nil {
|
||||
ok = true
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, badger.ErrKeyNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
})
|
||||
return ok, err
|
||||
}
|
||||
|
||||
// postInTxn is the internal helper used by iteration paths to fetch a full
|
||||
// PostRecord without opening a new View transaction.
|
||||
func (c *Chain) postInTxn(txn *badger.Txn, postID string) (*PostRecord, error) {
|
||||
item, err := txn.Get([]byte(prefixPost + postID))
|
||||
if errors.Is(err, badger.ErrKeyNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var p PostRecord
|
||||
if err := item.Value(func(val []byte) error {
|
||||
return json.Unmarshal(val, &p)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// WalletBinding returns the payout wallet pub key bound to a node, or "" if none.
|
||||
@@ -741,41 +878,197 @@ func (c *Chain) applyTx(txn *badger.Txn, tx *Transaction) (uint64, error) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
case EventCreateChannel:
|
||||
var p CreateChannelPayload
|
||||
// ── Feed events (v2.0.0) ──────────────────────────────────────────
|
||||
case EventCreatePost:
|
||||
var p CreatePostPayload
|
||||
if err := json.Unmarshal(tx.Payload, &p); err != nil {
|
||||
return 0, fmt.Errorf("%w: CREATE_CHANNEL bad payload: %v", ErrTxFailed, err)
|
||||
return 0, fmt.Errorf("%w: CREATE_POST bad payload: %v", ErrTxFailed, err)
|
||||
}
|
||||
if p.PostID == "" {
|
||||
return 0, fmt.Errorf("%w: CREATE_POST: post_id required", ErrTxFailed)
|
||||
}
|
||||
if len(p.ContentHash) != 32 {
|
||||
return 0, fmt.Errorf("%w: CREATE_POST: content_hash must be 32 bytes", ErrTxFailed)
|
||||
}
|
||||
if p.HostingRelay == "" {
|
||||
return 0, fmt.Errorf("%w: CREATE_POST: hosting_relay required", ErrTxFailed)
|
||||
}
|
||||
if p.Size == 0 || p.Size > MaxPostSize {
|
||||
return 0, fmt.Errorf("%w: CREATE_POST: size %d out of range (0, %d]",
|
||||
ErrTxFailed, p.Size, MaxPostSize)
|
||||
}
|
||||
if p.ReplyTo != "" && p.QuoteOf != "" {
|
||||
return 0, fmt.Errorf("%w: CREATE_POST: reply_to and quote_of are mutually exclusive", ErrTxFailed)
|
||||
}
|
||||
// Duplicate check — same post_id may only commit once.
|
||||
if _, err := txn.Get([]byte(prefixPost + p.PostID)); err == nil {
|
||||
return 0, fmt.Errorf("%w: CREATE_POST: post %s already exists", ErrTxFailed, p.PostID)
|
||||
}
|
||||
// Fee formula: BasePostFee + size × PostByteFee. tx.Fee carries the
|
||||
// full amount; we validate it matches and the sender can afford it.
|
||||
expectedFee := BasePostFee + p.Size*PostByteFee
|
||||
if tx.Fee < expectedFee {
|
||||
return 0, fmt.Errorf("%w: CREATE_POST: fee %d < required %d (base %d + %d × %d bytes)",
|
||||
ErrTxFailed, tx.Fee, expectedFee, BasePostFee, PostByteFee, p.Size)
|
||||
}
|
||||
if err := c.debitBalance(txn, tx.From, tx.Fee); err != nil {
|
||||
return 0, fmt.Errorf("CREATE_CHANNEL debit: %w", err)
|
||||
return 0, fmt.Errorf("CREATE_POST debit: %w", err)
|
||||
}
|
||||
val, _ := json.Marshal(p)
|
||||
if err := txn.Set([]byte(prefixChannel+p.ChannelID), val); err != nil {
|
||||
// Full fee goes to the hosting relay (storage compensation). No
|
||||
// validator cut on posts — validators earn from other tx types. This
|
||||
// incentivises nodes to actually host posts.
|
||||
relayTarget, err := c.resolveRewardTarget(txn, p.HostingRelay)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := c.creditBalance(txn, relayTarget, tx.Fee); err != nil {
|
||||
return 0, fmt.Errorf("credit hosting relay: %w", err)
|
||||
}
|
||||
rec := PostRecord{
|
||||
PostID: p.PostID,
|
||||
Author: tx.From,
|
||||
ContentHash: p.ContentHash,
|
||||
Size: p.Size,
|
||||
HostingRelay: p.HostingRelay,
|
||||
ReplyTo: p.ReplyTo,
|
||||
QuoteOf: p.QuoteOf,
|
||||
CreatedAt: tx.Timestamp.Unix(),
|
||||
FeeUT: tx.Fee,
|
||||
}
|
||||
recBytes, _ := json.Marshal(rec)
|
||||
if err := txn.Set([]byte(prefixPost+p.PostID), recBytes); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Chrono index — allows PostsByAuthor to list newest-first in O(N).
|
||||
idxKey := fmt.Sprintf("%s%s:%020d:%s", prefixPostByAuthor, tx.From, rec.CreatedAt, p.PostID)
|
||||
if err := txn.Set([]byte(idxKey), []byte{}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
case EventAddMember:
|
||||
var p AddMemberPayload
|
||||
case EventDeletePost:
|
||||
var p DeletePostPayload
|
||||
if err := json.Unmarshal(tx.Payload, &p); err != nil {
|
||||
return 0, fmt.Errorf("%w: ADD_MEMBER bad payload: %v", ErrTxFailed, err)
|
||||
return 0, fmt.Errorf("%w: DELETE_POST bad payload: %v", ErrTxFailed, err)
|
||||
}
|
||||
if p.ChannelID == "" {
|
||||
return 0, fmt.Errorf("%w: ADD_MEMBER: channel_id required", ErrTxFailed)
|
||||
if p.PostID == "" {
|
||||
return 0, fmt.Errorf("%w: DELETE_POST: post_id required", ErrTxFailed)
|
||||
}
|
||||
if _, err := txn.Get([]byte(prefixChannel + p.ChannelID)); err != nil {
|
||||
item, err := txn.Get([]byte(prefixPost + p.PostID))
|
||||
if errors.Is(err, badger.ErrKeyNotFound) {
|
||||
return 0, fmt.Errorf("%w: DELETE_POST: post %s not found", ErrTxFailed, p.PostID)
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var rec PostRecord
|
||||
if err := item.Value(func(val []byte) error { return json.Unmarshal(val, &rec) }); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if rec.Author != tx.From {
|
||||
return 0, fmt.Errorf("%w: DELETE_POST: only author can delete", ErrTxFailed)
|
||||
}
|
||||
if err := c.debitBalance(txn, tx.From, tx.Fee); err != nil {
|
||||
return 0, fmt.Errorf("DELETE_POST debit: %w", err)
|
||||
}
|
||||
rec.Deleted = true
|
||||
val, _ := json.Marshal(rec)
|
||||
if err := txn.Set([]byte(prefixPost+p.PostID), val); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
case EventFollow:
|
||||
if tx.To == "" {
|
||||
return 0, fmt.Errorf("%w: FOLLOW: target (to) is required", ErrTxFailed)
|
||||
}
|
||||
if tx.To == tx.From {
|
||||
return 0, fmt.Errorf("%w: FOLLOW: cannot follow yourself", ErrTxFailed)
|
||||
}
|
||||
if err := c.debitBalance(txn, tx.From, tx.Fee); err != nil {
|
||||
return 0, fmt.Errorf("FOLLOW debit: %w", err)
|
||||
}
|
||||
// follow:<follower>:<target> + reverse index followin:<target>:<follower>
|
||||
fKey := []byte(prefixFollow + tx.From + ":" + tx.To)
|
||||
if _, err := txn.Get(fKey); err == nil {
|
||||
return 0, fmt.Errorf("%w: FOLLOW: already following", ErrTxFailed)
|
||||
}
|
||||
if err := txn.Set(fKey, []byte{}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := txn.Set([]byte(prefixFollowInbound+tx.To+":"+tx.From), []byte{}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
case EventUnfollow:
|
||||
if tx.To == "" {
|
||||
return 0, fmt.Errorf("%w: UNFOLLOW: target (to) is required", ErrTxFailed)
|
||||
}
|
||||
if err := c.debitBalance(txn, tx.From, tx.Fee); err != nil {
|
||||
return 0, fmt.Errorf("UNFOLLOW debit: %w", err)
|
||||
}
|
||||
fKey := []byte(prefixFollow + tx.From + ":" + tx.To)
|
||||
if _, err := txn.Get(fKey); err != nil {
|
||||
if errors.Is(err, badger.ErrKeyNotFound) {
|
||||
return 0, fmt.Errorf("%w: ADD_MEMBER: channel %q not found", ErrTxFailed, p.ChannelID)
|
||||
return 0, fmt.Errorf("%w: UNFOLLOW: not following", ErrTxFailed)
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
if err := txn.Delete(fKey); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := txn.Delete([]byte(prefixFollowInbound + tx.To + ":" + tx.From)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
case EventLikePost:
|
||||
var p LikePostPayload
|
||||
if err := json.Unmarshal(tx.Payload, &p); err != nil {
|
||||
return 0, fmt.Errorf("%w: LIKE_POST bad payload: %v", ErrTxFailed, err)
|
||||
}
|
||||
if p.PostID == "" {
|
||||
return 0, fmt.Errorf("%w: LIKE_POST: post_id required", ErrTxFailed)
|
||||
}
|
||||
if _, err := txn.Get([]byte(prefixPost + p.PostID)); err != nil {
|
||||
if errors.Is(err, badger.ErrKeyNotFound) {
|
||||
return 0, fmt.Errorf("%w: LIKE_POST: post %s not found", ErrTxFailed, p.PostID)
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
lKey := []byte(prefixLike + p.PostID + ":" + tx.From)
|
||||
if _, err := txn.Get(lKey); err == nil {
|
||||
return 0, fmt.Errorf("%w: LIKE_POST: already liked", ErrTxFailed)
|
||||
}
|
||||
if err := c.debitBalance(txn, tx.From, tx.Fee); err != nil {
|
||||
return 0, fmt.Errorf("LIKE_POST debit: %w", err)
|
||||
}
|
||||
if err := txn.Set(lKey, []byte{}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := bumpLikeCount(txn, p.PostID, +1); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
case EventUnlikePost:
|
||||
var p UnlikePostPayload
|
||||
if err := json.Unmarshal(tx.Payload, &p); err != nil {
|
||||
return 0, fmt.Errorf("%w: UNLIKE_POST bad payload: %v", ErrTxFailed, err)
|
||||
}
|
||||
if p.PostID == "" {
|
||||
return 0, fmt.Errorf("%w: UNLIKE_POST: post_id required", ErrTxFailed)
|
||||
}
|
||||
lKey := []byte(prefixLike + p.PostID + ":" + tx.From)
|
||||
if _, err := txn.Get(lKey); err != nil {
|
||||
if errors.Is(err, badger.ErrKeyNotFound) {
|
||||
return 0, fmt.Errorf("%w: UNLIKE_POST: not liked", ErrTxFailed)
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
if err := c.debitBalance(txn, tx.From, tx.Fee); err != nil {
|
||||
return 0, fmt.Errorf("ADD_MEMBER debit: %w", err)
|
||||
return 0, fmt.Errorf("UNLIKE_POST debit: %w", err)
|
||||
}
|
||||
member := tx.To
|
||||
if member == "" {
|
||||
member = tx.From
|
||||
if err := txn.Delete(lKey); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := txn.Set([]byte(fmt.Sprintf("%s%s:%s", prefixChanMember, p.ChannelID, member)), []byte{}); err != nil {
|
||||
if err := bumpLikeCount(txn, p.PostID, -1); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -2336,6 +2629,35 @@ func (c *Chain) isValidatorTxn(txn *badger.Txn, pubKey string) (bool, error) {
|
||||
|
||||
// verifyEd25519 verifies an Ed25519 signature without importing the identity package
|
||||
// (which would create a circular dependency).
|
||||
// bumpLikeCount adjusts the cached like counter for a post. delta = ±1.
|
||||
// Clamps at zero so a corrupt unlike without prior like can't underflow.
|
||||
func bumpLikeCount(txn *badger.Txn, postID string, delta int64) error {
|
||||
key := []byte(prefixLikeCount + postID)
|
||||
var cur uint64
|
||||
item, err := txn.Get(key)
|
||||
if err == nil {
|
||||
if verr := item.Value(func(val []byte) error {
|
||||
if len(val) == 8 {
|
||||
cur = binary.BigEndian.Uint64(val)
|
||||
}
|
||||
return nil
|
||||
}); verr != nil {
|
||||
return verr
|
||||
}
|
||||
} else if !errors.Is(err, badger.ErrKeyNotFound) {
|
||||
return err
|
||||
}
|
||||
switch {
|
||||
case delta < 0 && cur > 0:
|
||||
cur--
|
||||
case delta > 0:
|
||||
cur++
|
||||
}
|
||||
var buf [8]byte
|
||||
binary.BigEndian.PutUint64(buf[:], cur)
|
||||
return txn.Set(key, buf[:])
|
||||
}
|
||||
|
||||
func verifyEd25519(pubKeyHex string, msg, sig []byte) (bool, error) {
|
||||
pubBytes, err := hex.DecodeString(pubKeyHex)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user