📢 Steem Go Libraries Update: steemutil v0.0.22–v0.0.25 & steemgosdk v0.0.23
Date: 2026-07-14
Affected modules:github.com/steemit/steemutil,github.com/steemit/steemgosdk
Audience: Go developers building on Steem (conveyor, wallets, indexers, bots)
A batch of releases across our two Go libraries lays the groundwork for the
upcoming conveyor Go rewrite. steemutil gained correctness fixes to its
protocol encoder, real RPC signature verification, and chain-aligned Asset/Price
primitives. steemgosdk builds on top of these to ship server-side __signed
verification and typed chain-query wrappers that conveyor needs.
| Module | From → To | Highlights |
|---|---|---|
steemutil | v0.0.21 → v0.0.25 | Encoder fixes (pointer-optional, pubkey serialization), rpc.VerifySignedRpc, integer Asset/Price matching steemd semantics |
steemgosdk | v0.0.22 → v0.0.23 | API.VerifySignedRequest, eight typed chain-query wrappers, bumps steemutil to v0.0.25 |
No new third-party dependencies were introduced in any of these releases.
Both libraries remain on the Go standard library + btcec/btcutil (steemutil)
and pkg/errors only.
steemutil releases
v0.0.22 — Encoder: stop treating all pointer fields as optional<T>
PR #10. The protocol encoder previously treated every pointer field as
optional<T>, emitting an extra presence tag byte even for fields that are
plain required pointers. This produced malformed serialization for several
operation types. The encoder now only applies optional encoding to fields that
are genuinely declared optional.
- Impact: Fixes transaction/operation serialization for affected types.
If you were hand-padding or working around extra bytes in serialized ops,
remove those workarounds. - Includes golden-hex serialization tests covering the optional path.
v0.0.23 — Encode public_key_type as 33-byte binary blob
PR #11. public_key_type fields were serialized incorrectly (as a
hex/varint form), which broke any operation carrying a public key — most
notably account_update and account_create. They are now encoded as the
canonical 33-byte compressed public key blob, matching steemd's wire
format.
- Impact: Fixes signing/broadcast of key-bearing operations.
- Includes golden-hex tests for all public-key-bearing fields.
v0.0.24 — RPC signature verification (VerifySignedRpc)
PR #12. This is the core of the server-side __signed authentication that
conveyor (and any JSON-RPC 2.0 server authenticating callers by Steem account
ownership) needs. It mirrors @steemit/koa-jsonrpc's verifier byte-for-byte.
New exported API:
// rpc package
type AccountFetcher func(account string) (api.Authority, error)
func VerifySignedRpc(message []byte, signatures []string, account string,
accountFetcher AccountFetcher) error
Key design points:
- Posting authority only. Only the account's posting key is checked
(active/owner/memo ignored), matching the JS verifier. - Single-key, single-signature. Accounts with multiple posting keys or
requests with multiple signatures are rejected (Unsupported posting key configuration/Multisig not supported) — the JS verifier has the same
limitation. - Exact error wording matches koa-jsonrpc so Go and JS servers
reject/accept the same requests. AccountFetcheris a callback (returnsapi.Authority) so therpcpackage
stays free of any chain-access dependency; the caller wires it to
get_accounts.- The compact-signature byte layout used by dsteem/libcrypto
(recovery + 31) is byte-identical to btcec's compressed layout
(27 + recoveryId + 4) for compressed keys, so verification needed no
conversion glue — just a format guard (wif.ValidateCompactSignature). rpc.Validatealready handled the envelope (jsonrpc/method, 60-second
freshness, nonce, message digest); v0.0.24 plugs real crypto verification
into it.
Also shipped: custom MarshalJSON/UnmarshalJSON for KeyAuth and
AccountAuthEntry, so the wire form [["STMxxx", 1], ...] flattens into typed
fields.
v0.0.25 — Asset/Price integer primitives
PR #13. Adds Asset, Price, and ComputePrices to protocol/api, with
arithmetic that matches steemd's C++ semantics exactly — zero floating point.
This replaces the float64 approach used by dsteem (a JS limitation, not a
correct design). Assets are int64 atom units; price conversion uses
math/big.Int to replicate steemd's 128-bit cross-multiply-divide.
// protocol/api package
type Asset struct {
Amount int64 // atom units, e.g. 1500 == "1.500 STEEM"
Symbol string // STEEM | SBD | VESTS | TESTS | TBD
}
type Price struct {
Base, Quote Asset
}
func ParseAsset(s string) (Asset, error)
func ParsePrice(base, quote string) (Price, error)
func (p Price) Convert(a Asset) (Asset, error) // replicates steemd asset*price
func (p Price) Compare(q Price) int // cross-multiply, no division
func (p Price) Invert() Price
func (a Asset) Add(b Asset) (Asset, error) // overflow-checked
func (a Asset) Sub(b Asset) (Asset, error) // rejects negatives
Why integer, and why math/big:
- steemd stores assets as
share_type(safe<int64_t>); prices are ratios of
twoassets;asset * pricecomputes(uint128(a) * numerator) / denominator
with a high-bits-zero overflow check. There is nodoubleanywhere in
steemd's consensus code. Asset.Amountis thereforeint64.Price.Convertdoes the same
cross-multiply-divide inmath/big.Int(which trivially covers 128 bits),
truncating toward zero and asserting the result fits — bit-identical results
to the C++.- Consequence: no
0.1 + 0.2 != 0.3class of errors. Amounts are exact.
ComputePrices wraps the three price computations conveyor's get_prices
needs (internal market SBD, witness USD feed, VESTS-per-STEEM), returning exact
Asset/Price values rather than float64 display numbers. Conveyor decides
how/whether to round for display.
Precision (steemd-aligned): STEEM/SBD/TESTS/TBD = 3 decimals; VESTS = 6.
MaxSatoshis = 2^62 - 1.
Wire-bridge helpers OrderPrice.ToPrice() and
CurrentMedianHistoryPrice.ToPrice() were added so the existing string-asset
wire structs feed straight into the new primitives.
steemgosdk v0.0.23
PR #11. Bumps steemutil to v0.0.25 and adds the two pieces conveyor needed
that were missing from the SDK.
G1: Server-side __signed verification
func (a *API) VerifySignedRequest(signedReq *rpc.SignedRequest) (
params []interface{}, account string, err error)
This is the server-side counterpart to the existing SignedCall (which only
signs and sends). It builds an AccountFetcher that resolves the account's
posting authority via get_accounts and returns accts[0].Posting directly —
its type (protocolapi.Authority) is exactly what AccountFetcher expects, so
there's zero conversion glue. The request is then validated with
rpc.Validate + rpc.VerifySignedRpc.
G2: Typed chain-query wrappers
Eight typed wrappers over CallWithResult, replacing the hand-written result
structs callers previously needed:
| Method | RPC | Params (positional array) |
|---|---|---|
GetAccounts | condenser_api.get_accounts | [["n1","n2"]] |
GetFollowCount | condenser_api.get_follow_count | [account] |
GetFollowers | condenser_api.get_followers | [account, start, type, limit] |
GetFollowing | condenser_api.get_following | [account, start, type, limit] |
GetAccountHistory | condenser_api.get_account_history | [account, from, limit] |
LookupAccounts | condenser_api.lookup_accounts | [lowerBound, limit] |
GetOrderBook | condenser_api.get_order_book | [limit] |
GetFeedHistory | condenser_api.get_feed_history | [] |
Result structs reuse steemutil's protocol/api types; only AccountHistoryEntry
is defined locally (its wire form is a [index, {op:[type,payload],timestamp}]
tuple, with a custom UnmarshalJSON).
get_account_history pagination matches conveyor's
accountHistoryGenerator: first page uses from = -1, limit = 1000 (-1 =
newest); the SDK returns one page and the caller drives the paging loop.
A note on the condenser_api vs database_api choice
GetOrderBook and GetFeedHistory target condenser_api, not
database_api. This is deliberate: condenser_api handlers take positional
arrays, which is what the SDK's jsonrpc2 layer (RpcSendData.Params []any)
emits. database_api handlers take named objects ({"limit": N}), which a
[]any slice cannot represent. An audit confirmed the condenser_api shapes
are accepted by api.steemit.com, and ComputePrices runs end-to-end on real
data.
Both APIs are reachable via steemd's preferred dotted form
(method: "api.method"); the legacy method: "call" form (api/method packed
into params) is retained only for backwards compatibility.
Tests
- Offline unit tests (default
go test):httptest.Server-based,
deterministic, no network dependency. Includes params-shape regression tests
that assert every wrapper emits the correct positional-array shape and
targets the correct API. - Integration tests (
go test -tags=integration): run against
api.steemit.com, verifying the wire shapes are accepted by a real node and
ComputePricesproduces sane values (e.g. 1.000 STEEM → ~1622 VESTS).
Upgrading
# steemutil consumers
go get github.com/steemit/[email protected]
# steemgosdk consumers
go get github.com/steemit/[email protected]
Breaking changes
None at the API level. The encoder fixes (steemutil v0.0.22, v0.0.23) change
the bytes produced for affected operation/key types — this is a bug fix
(meaningful output was previously malformed), but if you have persisted or
pinned any serialized bytes from older versions for those specific types, they
will differ after upgrading.
Go version
Both modules require Go 1.18+ (unchanged).
What's next
These releases unblock the conveyor Go rewrite (Gin + steemgosdk). The
authentication path (VerifySignedRequest), the high-frequency chain queries
(G2), and the price arithmetic (steemutil ComputePrices) are all now
available off the shelf.
I am using these on Steem Virtual Machine for Bridge Module Packed inside the chain Binary itself . Great work so far .
0.00 SBD,
0.67 STEEM,
0.67 SP
Upvoted! Thank you for supporting witness @jswit.
Good job