From f19054f7b1c72d516404f3be5218ab07d49be1fe Mon Sep 17 00:00:00 2001 From: Snider Date: Mon, 16 Mar 2026 22:20:06 +0000 Subject: [PATCH 1/2] chore: sync dependencies for v0.1.3 Co-Authored-By: Virgil --- go.mod | 21 +++++++++------------ go.sum | 38 ++++++++++++++++---------------------- 2 files changed, 25 insertions(+), 34 deletions(-) diff --git a/go.mod b/go.mod index 803a716..71f57b2 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,12 @@ module forge.lthn.ai/core/go-blockchain go 1.26.0 require ( - forge.lthn.ai/core/cli v0.3.1 - forge.lthn.ai/core/go-p2p v0.1.3 - forge.lthn.ai/core/go-process v0.2.3 - forge.lthn.ai/core/go-store v0.1.6 + forge.lthn.ai/core/cli v0.3.5 + forge.lthn.ai/core/go-io v0.1.5 + forge.lthn.ai/core/go-log v0.0.4 + forge.lthn.ai/core/go-p2p v0.1.5 + forge.lthn.ai/core/go-process v0.2.7 + forge.lthn.ai/core/go-store v0.1.8 github.com/charmbracelet/bubbletea v1.3.10 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 @@ -15,12 +17,8 @@ require ( require ( forge.lthn.ai/core/go v0.3.1 // indirect - forge.lthn.ai/core/go-crypt v0.1.7 // indirect - forge.lthn.ai/core/go-i18n v0.1.4 // indirect - forge.lthn.ai/core/go-inference v0.1.4 // indirect - forge.lthn.ai/core/go-io v0.1.2 // indirect - forge.lthn.ai/core/go-log v0.0.4 // indirect - github.com/ProtonMail/go-crypto v1.4.0 // indirect + forge.lthn.ai/core/go-i18n v0.1.6 // indirect + forge.lthn.ai/core/go-inference v0.1.5 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect @@ -29,7 +27,6 @@ require ( github.com/charmbracelet/x/term v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect - github.com/cloudflare/circl v1.6.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect @@ -55,5 +52,5 @@ require ( modernc.org/libc v1.70.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.46.1 // indirect + modernc.org/sqlite v1.46.2 // indirect ) diff --git a/go.sum b/go.sum index ba88920..dcdc2da 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,21 @@ -forge.lthn.ai/core/cli v0.3.1 h1:ZpHhaDrdbaV98JDxj/f0E5nytYk9tTMRu3qohGyK4M0= -forge.lthn.ai/core/cli v0.3.1/go.mod h1:28cOl9eK0H033Otkjrv9f/QCmtHcJl+IIx4om8JskOg= +forge.lthn.ai/core/cli v0.3.5 h1:P7yK0DmSA1QnUMFuCjJZf/fk/akKPIxopQ6OwD8Sar8= +forge.lthn.ai/core/cli v0.3.5/go.mod h1:SeArHx+hbpX5iZqgASCD7Q1EDoc6uaaGiGBotmNzIx4= forge.lthn.ai/core/go v0.3.1 h1:5FMTsUhLcxSr07F9q3uG0Goy4zq4eLivoqi8shSY4UM= forge.lthn.ai/core/go v0.3.1/go.mod h1:gE6c8h+PJ2287qNhVUJ5SOe1kopEwHEquvinstpuyJc= -forge.lthn.ai/core/go-crypt v0.1.7 h1:tyDFnXjEksHFQpkFwCpEn+x7zvwh4LnaU+/fP3WmqZc= -forge.lthn.ai/core/go-crypt v0.1.7/go.mod h1:mQdr6K8lWOcyHmSEW24vZPTThQF8fteVgZi8CO+Ko3Y= -forge.lthn.ai/core/go-i18n v0.1.4 h1:zOHUUJDgRo88/3tj++kN+VELg/buyZ4T2OSdG3HBbLQ= -forge.lthn.ai/core/go-i18n v0.1.4/go.mod h1:aDyAfz7MMgWYgLkZCptfFmZ7jJg3ocwjEJ1WkJSvv4U= -forge.lthn.ai/core/go-inference v0.1.4 h1:fuAgWbqsEDajHniqAKyvHYbRcBrkGEiGSqR2pfTMRY0= -forge.lthn.ai/core/go-inference v0.1.4/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw= -forge.lthn.ai/core/go-io v0.1.2 h1:q8hj2jtOFqAgHlBr5wsUAOXtaFkxy9gqGrQT/il0WYA= -forge.lthn.ai/core/go-io v0.1.2/go.mod h1:PbNKW1Q25ywSOoQXeGdQHbV5aiIrTXvHIQ5uhplA//g= +forge.lthn.ai/core/go-i18n v0.1.6 h1:Z9h6sEZsgJmWlkkq3ZPZyfgWipeeqN5lDCpzltpamHU= +forge.lthn.ai/core/go-i18n v0.1.6/go.mod h1:C6CbwdN7sejTx/lbutBPrxm77b8paMHBO6uHVLHOdqQ= +forge.lthn.ai/core/go-inference v0.1.5 h1:Az/Euv1DusJQJz/Eca0Ey7sVXQkFLPHW0TBrs9g+Qwg= +forge.lthn.ai/core/go-inference v0.1.5/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw= +forge.lthn.ai/core/go-io v0.1.5 h1:+XJ1YhaGGFLGtcNbPtVlndTjk+pO0Ydi2hRDj5/cHOM= +forge.lthn.ai/core/go-io v0.1.5/go.mod h1:FRtXSsi8W+U9vewCU+LBAqqbIj3wjXA4dBdSv3SAtWI= forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0= forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw= -forge.lthn.ai/core/go-p2p v0.1.3 h1:XbETiHrYTDiJTq6EAxdU+MJF1l5UxEQE14wJ7G7FOVc= -forge.lthn.ai/core/go-p2p v0.1.3/go.mod h1:F2M4qIzkixQpZEoOEtNaB4rhmi1WQKbR7JqVzGA1r80= -forge.lthn.ai/core/go-process v0.2.3 h1:/ERqRYHgCNZjNT9NMinAAJJGJWSsHuCTiHFNEm6nTPY= -forge.lthn.ai/core/go-process v0.2.3/go.mod h1:gVTbxL16ccUIexlFcyDtCy7LfYvD8Rtyzfo8bnXAXrU= -forge.lthn.ai/core/go-store v0.1.6 h1:7T+K5cciXOaWRxge0WnGkt0PcK3epliWBa1G2FLEuac= -forge.lthn.ai/core/go-store v0.1.6/go.mod h1:/2vqaAn+HgGU14N29B+vIfhjIsBzy7RC+AluI6BIUKI= -github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ= -github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= +forge.lthn.ai/core/go-p2p v0.1.5 h1:/jEhkz3HYCrRPJ37JoXPnIX+UsC3YhX7PRoXp44n7TA= +forge.lthn.ai/core/go-p2p v0.1.5/go.mod h1:d32MQdcWRDJYlOnWsaHLbxxz+P9DLxPOBEgz3tsemW4= +forge.lthn.ai/core/go-process v0.2.7 h1:yl7jOxzDqWpJd/ZvJ/Ff6bHgPFLA1ZYU5UDcsz3AzLM= +forge.lthn.ai/core/go-process v0.2.7/go.mod h1:I6x11UNaZbU3k0FWUaSlPRTE4YZk/lWIjiODm/8Jr9c= +forge.lthn.ai/core/go-store v0.1.8 h1:jeFqxilifa/hXtQqCeXX/+Vwy6M/XZE7uCP8XQ0ercw= +forge.lthn.ai/core/go-store v0.1.8/go.mod h1:DJocTeTCjFBPn5ppQT/IDheFJhOfwlHeoxEUtDH07zE= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= @@ -38,8 +34,6 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= -github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -139,8 +133,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= -modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/sqlite v1.46.2 h1:gkXQ6R0+AjxFC/fTDaeIVLbNLNrRoOK7YYVz5BKhTcE= +modernc.org/sqlite v1.46.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -- 2.45.3 From f27825cfc9ec2112cc83ab42830d27dc2048a987 Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 17 Mar 2026 08:34:04 +0000 Subject: [PATCH 2/2] fix(dx): audit and fix error handling, file I/O, wire compilation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLAUDE.md: update error handling guidance from fmt.Errorf to coreerr.E() and document go-io convention for file I/O - wire/transaction.go: fix TxOutTarget interface compilation — add encodeTarget/decodeTarget helpers with support for TxOutToKey, TxOutMultisig, and TxOutHTLC target types - wire/transaction.go: add HTLC and multisig input encode/decode (TxInputHTLC, TxInputMultisig) with string encoding helpers - wire/transaction.go: add asset operation tag constants (40, 49-51) and reader functions for HF5 confidential asset operations - consensus/block.go: replace fmt.Errorf with coreerr.E() for checkBlockVersion and ValidateTransactionInBlock - chain/ring.go: replace fmt.Errorf with coreerr.E() in GetRingOutputs - consensus/v2sig_test.go: replace os.ReadFile with coreio.Read - crypto/*.go: replace all fmt.Errorf and errors.New with coreerr.E() across keygen, pow, keyimage, signature, and clsag packages - types/types_test.go: add tests for HashFromHex, PublicKeyFromHex, IsZero, and String methods (types coverage 74.5% -> 89.1%) Co-Authored-By: Virgil --- CLAUDE.md | 4 +- chain/ring.go | 2 +- consensus/block.go | 6 +- consensus/v2sig_test.go | 8 +- crypto/clsag.go | 11 +- crypto/keygen.go | 13 +- crypto/keyimage.go | 5 +- crypto/pow.go | 4 +- crypto/signature.go | 7 +- types/types_test.go | 106 +++++++++++++ wire/transaction.go | 334 ++++++++++++++++++++++++++++++++++++++-- 11 files changed, 458 insertions(+), 42 deletions(-) create mode 100644 types/types_test.go diff --git a/CLAUDE.md b/CLAUDE.md index 8a6cb68..cc6067e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,8 +40,8 @@ go test -tags integration ./... # integration tests (need C++ te - `go test -race ./...`, `go vet ./...`, and `go mod tidy` must all pass before commit - Conventional commits: `type(scope): description` - Co-Author trailer: `Co-Authored-By: Charon ` -- Error strings: `package: description` format (e.g. `types: invalid hex for hash`) -- Error wrapping: `fmt.Errorf("package: description: %w", err)` +- Error handling: `coreerr.E("Caller", "description", err)` via `coreerr "forge.lthn.ai/core/go-log"` — not `fmt.Errorf` or `errors.New` +- File I/O: `coreio "forge.lthn.ai/core/go-io"` — not `os.ReadFile`/`os.WriteFile` - Import order: stdlib, then `golang.org/x`, then `forge.lthn.ai`, blank lines between groups - No emojis in code or comments diff --git a/chain/ring.go b/chain/ring.go index acee086..06645b0 100644 --- a/chain/ring.go +++ b/chain/ring.go @@ -38,7 +38,7 @@ func (c *Chain) GetRingOutputs(amount uint64, offsets []uint64) ([]types.PublicK case types.TxOutputBare: toKey, ok := out.Target.(types.TxOutToKey) if !ok { - return nil, fmt.Errorf("ring output %d: unsupported target type %T", i, out.Target) + return nil, coreerr.E("Chain.GetRingOutputs", fmt.Sprintf("ring output %d: unsupported target type %T", i, out.Target), nil) } pubs[i] = toKey.Key default: diff --git a/consensus/block.go b/consensus/block.go index 5345b6f..dfaf16b 100644 --- a/consensus/block.go +++ b/consensus/block.go @@ -152,8 +152,8 @@ func expectedBlockMajorVersion(forks []config.HardFork, height uint64) uint8 { func checkBlockVersion(blk *types.Block, forks []config.HardFork, height uint64) error { expected := expectedBlockMajorVersion(forks, height) if blk.MajorVersion != expected { - return fmt.Errorf("%w: got %d, want %d at height %d", - ErrBlockMajorVersion, blk.MajorVersion, expected, height) + return coreerr.E("checkBlockVersion", fmt.Sprintf("got %d, want %d at height %d", + blk.MajorVersion, expected, height), ErrBlockMajorVersion) } return nil } @@ -226,7 +226,7 @@ func IsPreHardforkFreeze(forks []config.HardFork, version uint8, height uint64) func ValidateTransactionInBlock(tx *types.Transaction, txBlob []byte, forks []config.HardFork, height uint64) error { // Pre-hardfork freeze: reject non-coinbase transactions in the freeze window. if !isCoinbase(tx) && IsPreHardforkFreeze(forks, config.HF5, height) { - return fmt.Errorf("%w: height %d is within HF5 freeze window", ErrPreHardforkFreeze, height) + return coreerr.E("ValidateTransactionInBlock", fmt.Sprintf("height %d is within HF5 freeze window", height), ErrPreHardforkFreeze) } return ValidateTransaction(tx, txBlob, forks, height) diff --git a/consensus/v2sig_test.go b/consensus/v2sig_test.go index 78f066c..774f9a2 100644 --- a/consensus/v2sig_test.go +++ b/consensus/v2sig_test.go @@ -8,9 +8,11 @@ package consensus import ( "bytes" "encoding/hex" - "os" + "strings" "testing" + coreio "forge.lthn.ai/core/go-io" + "forge.lthn.ai/core/go-blockchain/config" "forge.lthn.ai/core/go-blockchain/types" "forge.lthn.ai/core/go-blockchain/wire" @@ -21,10 +23,10 @@ import ( // loadTestTx loads and decodes a hex-encoded transaction from testdata. func loadTestTx(t *testing.T, filename string) *types.Transaction { t.Helper() - hexData, err := os.ReadFile(filename) + hexStr, err := coreio.Read(coreio.Local, filename) require.NoError(t, err, "read %s", filename) - blob, err := hex.DecodeString(string(bytes.TrimSpace(hexData))) + blob, err := hex.DecodeString(strings.TrimSpace(hexStr)) require.NoError(t, err, "decode hex") dec := wire.NewDecoder(bytes.NewReader(blob)) diff --git a/crypto/clsag.go b/crypto/clsag.go index 9791f60..7cdfa1c 100644 --- a/crypto/clsag.go +++ b/crypto/clsag.go @@ -8,8 +8,9 @@ package crypto import "C" import ( - "errors" "unsafe" + + coreerr "forge.lthn.ai/core/go-log" ) // PointMul8 multiplies a curve point by the cofactor 8. @@ -20,7 +21,7 @@ func PointMul8(pk [32]byte) ([32]byte, error) { (*C.uint8_t)(unsafe.Pointer(&result[0])), ) if rc != 0 { - return result, errors.New("crypto: point_mul8 failed") + return result, coreerr.E("PointMul8", "point_mul8 failed", nil) } return result, nil } @@ -34,7 +35,7 @@ func PointDiv8(pk [32]byte) ([32]byte, error) { (*C.uint8_t)(unsafe.Pointer(&result[0])), ) if rc != 0 { - return result, errors.New("crypto: point_div8 failed") + return result, coreerr.E("PointDiv8", "point_div8 failed", nil) } return result, nil } @@ -48,7 +49,7 @@ func PointSub(a, b [32]byte) ([32]byte, error) { (*C.uint8_t)(unsafe.Pointer(&result[0])), ) if rc != 0 { - return result, errors.New("crypto: point_sub failed") + return result, coreerr.E("PointSub", "point_sub failed", nil) } return result, nil } @@ -81,7 +82,7 @@ func GenerateCLSAGGG(hash [32]byte, ring []byte, ringSize int, (*C.uint8_t)(unsafe.Pointer(&sig[0])), ) if rc != 0 { - return nil, errors.New("crypto: generate_CLSAG_GG failed") + return nil, coreerr.E("GenerateCLSAGGG", "generate_CLSAG_GG failed", nil) } return sig, nil } diff --git a/crypto/keygen.go b/crypto/keygen.go index 5c11dd3..66f3065 100644 --- a/crypto/keygen.go +++ b/crypto/keygen.go @@ -8,9 +8,10 @@ package crypto import "C" import ( - "errors" "fmt" "unsafe" + + coreerr "forge.lthn.ai/core/go-log" ) // GenerateKeys creates a new random key pair. @@ -20,7 +21,7 @@ func GenerateKeys() (pub [32]byte, sec [32]byte, err error) { (*C.uint8_t)(unsafe.Pointer(&sec[0])), ) if rc != 0 { - err = fmt.Errorf("crypto: generate_keys failed (rc=%d)", rc) + err = coreerr.E("GenerateKeys", fmt.Sprintf("generate_keys failed (rc=%d)", rc), nil) } return } @@ -33,7 +34,7 @@ func SecretToPublic(sec [32]byte) ([32]byte, error) { (*C.uint8_t)(unsafe.Pointer(&pub[0])), ) if rc != 0 { - return pub, fmt.Errorf("crypto: secret_to_public failed (rc=%d)", rc) + return pub, coreerr.E("SecretToPublic", fmt.Sprintf("secret_to_public failed (rc=%d)", rc), nil) } return pub, nil } @@ -52,7 +53,7 @@ func GenerateKeyDerivation(pub [32]byte, sec [32]byte) ([32]byte, error) { (*C.uint8_t)(unsafe.Pointer(&d[0])), ) if rc != 0 { - return d, errors.New("crypto: generate_key_derivation failed") + return d, coreerr.E("GenerateKeyDerivation", "generate_key_derivation failed", nil) } return d, nil } @@ -67,7 +68,7 @@ func DerivePublicKey(derivation [32]byte, index uint64, base [32]byte) ([32]byte (*C.uint8_t)(unsafe.Pointer(&derived[0])), ) if rc != 0 { - return derived, errors.New("crypto: derive_public_key failed") + return derived, coreerr.E("DerivePublicKey", "derive_public_key failed", nil) } return derived, nil } @@ -82,7 +83,7 @@ func DeriveSecretKey(derivation [32]byte, index uint64, base [32]byte) ([32]byte (*C.uint8_t)(unsafe.Pointer(&derived[0])), ) if rc != 0 { - return derived, errors.New("crypto: derive_secret_key failed") + return derived, coreerr.E("DeriveSecretKey", "derive_secret_key failed", nil) } return derived, nil } diff --git a/crypto/keyimage.go b/crypto/keyimage.go index 961e116..26c74ee 100644 --- a/crypto/keyimage.go +++ b/crypto/keyimage.go @@ -8,8 +8,9 @@ package crypto import "C" import ( - "errors" "unsafe" + + coreerr "forge.lthn.ai/core/go-log" ) // GenerateKeyImage computes the key image for a public/secret key pair. @@ -22,7 +23,7 @@ func GenerateKeyImage(pub [32]byte, sec [32]byte) ([32]byte, error) { (*C.uint8_t)(unsafe.Pointer(&ki[0])), ) if rc != 0 { - return ki, errors.New("crypto: generate_key_image failed") + return ki, coreerr.E("GenerateKeyImage", "generate_key_image failed", nil) } return ki, nil } diff --git a/crypto/pow.go b/crypto/pow.go index be8e84e..74f0a0a 100644 --- a/crypto/pow.go +++ b/crypto/pow.go @@ -10,6 +10,8 @@ import "C" import ( "fmt" "unsafe" + + coreerr "forge.lthn.ai/core/go-log" ) // RandomXHash computes the RandomX PoW hash. The key is the cache @@ -23,7 +25,7 @@ func RandomXHash(key, input []byte) ([32]byte, error) { (*C.uint8_t)(unsafe.Pointer(&output[0])), ) if ret != 0 { - return output, fmt.Errorf("crypto: RandomX hash failed with code %d", ret) + return output, coreerr.E("RandomXHash", fmt.Sprintf("RandomX hash failed with code %d", ret), nil) } return output, nil } diff --git a/crypto/signature.go b/crypto/signature.go index 664639a..f9699db 100644 --- a/crypto/signature.go +++ b/crypto/signature.go @@ -8,8 +8,9 @@ package crypto import "C" import ( - "errors" "unsafe" + + coreerr "forge.lthn.ai/core/go-log" ) // GenerateSignature creates a standard (non-ring) signature. @@ -22,7 +23,7 @@ func GenerateSignature(hash [32]byte, pub [32]byte, sec [32]byte) ([64]byte, err (*C.uint8_t)(unsafe.Pointer(&sig[0])), ) if rc != 0 { - return sig, errors.New("crypto: generate_signature failed") + return sig, coreerr.E("GenerateSignature", "generate_signature failed", nil) } return sig, nil } @@ -60,7 +61,7 @@ func GenerateRingSignature(hash [32]byte, image [32]byte, pubs [][32]byte, (*C.uint8_t)(unsafe.Pointer(&flatSigs[0])), ) if rc != 0 { - return nil, errors.New("crypto: generate_ring_signature failed") + return nil, coreerr.E("GenerateRingSignature", "generate_ring_signature failed", nil) } sigs := make([][64]byte, n) diff --git a/types/types_test.go b/types/types_test.go new file mode 100644 index 0000000..cecc21d --- /dev/null +++ b/types/types_test.go @@ -0,0 +1,106 @@ +// Copyright (c) 2017-2026 Lethean (https://lt.hn) +// +// Licensed under the European Union Public Licence (EUPL) version 1.2. +// SPDX-License-Identifier: EUPL-1.2 + +package types + +import ( + "strings" + "testing" +) + +func TestHashFromHex_Good(t *testing.T) { + hexStr := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + h, err := HashFromHex(hexStr) + if err != nil { + t.Fatalf("HashFromHex: unexpected error: %v", err) + } + if h[0] != 0x01 || h[1] != 0x23 { + t.Errorf("HashFromHex: got [0]=%02x [1]=%02x, want 01 23", h[0], h[1]) + } + if h.String() != hexStr { + t.Errorf("String: got %q, want %q", h.String(), hexStr) + } +} + +func TestHashFromHex_Bad(t *testing.T) { + tests := []struct { + name string + input string + }{ + {"short", "0123"}, + {"invalid_chars", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"}, + {"odd_length", "012"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := HashFromHex(tt.input) + if err == nil { + t.Error("expected error") + } + }) + } +} + +func TestHash_IsZero_Good(t *testing.T) { + var zero Hash + if !zero.IsZero() { + t.Error("zero hash: IsZero() should be true") + } + + nonZero := Hash{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + if nonZero.IsZero() { + t.Error("non-zero hash: IsZero() should be false") + } +} + +func TestPublicKeyFromHex_Good(t *testing.T) { + hexStr := strings.Repeat("ab", 32) + pk, err := PublicKeyFromHex(hexStr) + if err != nil { + t.Fatalf("PublicKeyFromHex: unexpected error: %v", err) + } + for i := range pk { + if pk[i] != 0xAB { + t.Fatalf("PublicKeyFromHex: byte %d = %02x, want 0xAB", i, pk[i]) + } + } + if pk.String() != hexStr { + t.Errorf("String: got %q, want %q", pk.String(), hexStr) + } +} + +func TestPublicKeyFromHex_Bad(t *testing.T) { + _, err := PublicKeyFromHex("tooshort") + if err == nil { + t.Error("expected error for short hex") + } +} + +func TestPublicKey_IsZero_Good(t *testing.T) { + var zero PublicKey + if !zero.IsZero() { + t.Error("zero key: IsZero() should be true") + } + nonZero := PublicKey{1} + if nonZero.IsZero() { + t.Error("non-zero key: IsZero() should be false") + } +} + +func TestSecretKey_String_Good(t *testing.T) { + sk := SecretKey{0xFF} + s := sk.String() + if !strings.HasPrefix(s, "ff") { + t.Errorf("String: got %q, want prefix ff", s) + } +} + +func TestKeyImage_String_Good(t *testing.T) { + ki := KeyImage{0xDE} + s := ki.String() + if !strings.HasPrefix(s, "de") { + t.Errorf("String: got %q, want prefix de", s) + } +} diff --git a/wire/transaction.go b/wire/transaction.go index 54c04fb..bac34be 100644 --- a/wire/transaction.go +++ b/wire/transaction.go @@ -164,6 +164,18 @@ func encodeInputs(enc *Encoder, vin []types.TxInput) { encodeKeyOffsets(enc, v.KeyOffsets) enc.WriteBlob32((*[32]byte)(&v.KeyImage)) enc.WriteBytes(v.EtcDetails) + case types.TxInputHTLC: + // Wire order: HTLCOrigin string BEFORE parent fields (C++ quirk). + encodeString(enc, v.HTLCOrigin) + enc.WriteVarint(v.Amount) + encodeKeyOffsets(enc, v.KeyOffsets) + enc.WriteBlob32((*[32]byte)(&v.KeyImage)) + enc.WriteBytes(v.EtcDetails) + case types.TxInputMultisig: + enc.WriteVarint(v.Amount) + enc.WriteBlob32((*[32]byte)(&v.MultisigOutID)) + enc.WriteVarint(v.SigsCount) + enc.WriteBytes(v.EtcDetails) } } } @@ -195,6 +207,21 @@ func decodeInputs(dec *Decoder) []types.TxInput { dec.ReadBlob32((*[32]byte)(&in.KeyImage)) in.EtcDetails = decodeRawVariantVector(dec) vin = append(vin, in) + case types.InputTypeHTLC: + var in types.TxInputHTLC + in.HTLCOrigin = decodeString(dec) + in.Amount = dec.ReadVarint() + in.KeyOffsets = decodeKeyOffsets(dec) + dec.ReadBlob32((*[32]byte)(&in.KeyImage)) + in.EtcDetails = decodeRawVariantVector(dec) + vin = append(vin, in) + case types.InputTypeMultisig: + var in types.TxInputMultisig + in.Amount = dec.ReadVarint() + dec.ReadBlob32((*[32]byte)(&in.MultisigOutID)) + in.SigsCount = dec.ReadVarint() + in.EtcDetails = decodeRawVariantVector(dec) + vin = append(vin, in) default: dec.err = coreerr.E("decodeInputs", fmt.Sprintf("wire: unsupported input tag 0x%02x", tag), nil) return vin @@ -241,6 +268,87 @@ func decodeKeyOffsets(dec *Decoder) []types.TxOutRef { return refs } +// --- string encoding --- + +// encodeString writes a varint-prefixed string. +func encodeString(enc *Encoder, s string) { + enc.WriteVarint(uint64(len(s))) + if len(s) > 0 { + enc.WriteBytes([]byte(s)) + } +} + +// decodeString reads a varint-prefixed string. +func decodeString(dec *Decoder) string { + n := dec.ReadVarint() + if n == 0 || dec.Err() != nil { + return "" + } + data := dec.ReadBytes(int(n)) + if dec.Err() != nil { + return "" + } + return string(data) +} + +// --- output targets --- + +// encodeTarget serialises a txout_target_v variant (tag + fields). +func encodeTarget(enc *Encoder, target types.TxOutTarget) { + enc.WriteVariantTag(target.TargetType()) + switch t := target.(type) { + case types.TxOutToKey: + enc.WriteBlob32((*[32]byte)(&t.Key)) + enc.WriteUint8(t.MixAttr) + case types.TxOutMultisig: + enc.WriteVarint(t.MinimumSigs) + enc.WriteVarint(uint64(len(t.Keys))) + for i := range t.Keys { + enc.WriteBlob32((*[32]byte)(&t.Keys[i])) + } + case types.TxOutHTLC: + enc.WriteBlob32((*[32]byte)(&t.HTLCHash)) + enc.WriteUint8(t.Flags) + enc.WriteVarint(t.Expiration) + enc.WriteBlob32((*[32]byte)(&t.PKRedeem)) + enc.WriteBlob32((*[32]byte)(&t.PKRefund)) + } +} + +// decodeTarget deserialises a txout_target_v from the given tag. +// Returns nil for unsupported tags (caller should set dec.err). +func decodeTarget(dec *Decoder, tag uint8) types.TxOutTarget { + switch tag { + case types.TargetTypeToKey: + var t types.TxOutToKey + dec.ReadBlob32((*[32]byte)(&t.Key)) + t.MixAttr = dec.ReadUint8() + return t + case types.TargetTypeMultisig: + var t types.TxOutMultisig + t.MinimumSigs = dec.ReadVarint() + n := dec.ReadVarint() + if dec.Err() != nil { + return nil + } + t.Keys = make([]types.PublicKey, n) + for i := uint64(0); i < n; i++ { + dec.ReadBlob32((*[32]byte)(&t.Keys[i])) + } + return t + case types.TargetTypeHTLC: + var t types.TxOutHTLC + dec.ReadBlob32((*[32]byte)(&t.HTLCHash)) + t.Flags = dec.ReadUint8() + t.Expiration = dec.ReadVarint() + dec.ReadBlob32((*[32]byte)(&t.PKRedeem)) + dec.ReadBlob32((*[32]byte)(&t.PKRefund)) + return t + default: + return nil + } +} + // --- outputs --- // encodeOutputsV1 serialises v0/v1 outputs. In v0/v1, outputs are tx_out_bare @@ -251,10 +359,7 @@ func encodeOutputsV1(enc *Encoder, vout []types.TxOutput) { switch v := out.(type) { case types.TxOutputBare: enc.WriteVarint(v.Amount) - // Target is a variant (txout_target_v) - enc.WriteVariantTag(types.TargetTypeToKey) - enc.WriteBlob32((*[32]byte)(&v.Target.Key)) - enc.WriteUint8(v.Target.MixAttr) + encodeTarget(enc, v.Target) } } } @@ -272,14 +377,15 @@ func decodeOutputsV1(dec *Decoder) []types.TxOutput { if dec.Err() != nil { return vout } - switch tag { - case types.TargetTypeToKey: - dec.ReadBlob32((*[32]byte)(&out.Target.Key)) - out.Target.MixAttr = dec.ReadUint8() - default: + target := decodeTarget(dec, tag) + if dec.Err() != nil { + return vout + } + if target == nil { dec.err = coreerr.E("decodeOutputsV1", fmt.Sprintf("wire: unsupported target tag 0x%02x", tag), nil) return vout } + out.Target = target vout = append(vout, out) } return vout @@ -293,9 +399,7 @@ func encodeOutputsV2(enc *Encoder, vout []types.TxOutput) { switch v := out.(type) { case types.TxOutputBare: enc.WriteVarint(v.Amount) - enc.WriteVariantTag(types.TargetTypeToKey) - enc.WriteBlob32((*[32]byte)(&v.Target.Key)) - enc.WriteUint8(v.Target.MixAttr) + encodeTarget(enc, v.Target) case types.TxOutputZarcanum: enc.WriteBlob32((*[32]byte)(&v.StealthAddress)) enc.WriteBlob32((*[32]byte)(&v.ConcealingPoint)) @@ -323,13 +427,18 @@ func decodeOutputsV2(dec *Decoder) []types.TxOutput { var out types.TxOutputBare out.Amount = dec.ReadVarint() targetTag := dec.ReadVariantTag() - if targetTag == types.TargetTypeToKey { - dec.ReadBlob32((*[32]byte)(&out.Target.Key)) - out.Target.MixAttr = dec.ReadUint8() - } else { + if dec.Err() != nil { + return vout + } + target := decodeTarget(dec, targetTag) + if dec.Err() != nil { + return vout + } + if target == nil { dec.err = coreerr.E("decodeOutputsV2", fmt.Sprintf("wire: unsupported target tag 0x%02x", targetTag), nil) return vout } + out.Target = target vout = append(vout, out) case types.OutputTypeZarcanum: var out types.TxOutputZarcanum @@ -434,6 +543,14 @@ const ( tagZCAssetSurjectionProof = 46 // vector tagZCOutsRangeProof = 47 // bpp_serialized + aggregation_proof tagZCBalanceProof = 48 // generic_double_schnorr_sig_s (96 bytes) + + // Asset operation tags (HF5 confidential assets). + tagAssetOperationProof = 49 // asset_operation_proof + tagAssetOperationOwnershipProof = 50 // asset_operation_ownership_proof + tagAssetOperationOwnershipProofETH = 51 // asset_operation_ownership_proof_eth + + // Extra variant tags (asset operations). + tagAssetDescriptorOperation = 40 // asset_descriptor_operation ) // readVariantElementData reads the data portion of a variant element (after the @@ -510,6 +627,16 @@ func readVariantElementData(dec *Decoder, tag uint8) []byte { case tagZCBalanceProof: // generic_double_schnorr_sig_s (3 scalars = 96 bytes) return dec.ReadBytes(96) + // Asset operation variants (HF5) + case tagAssetDescriptorOperation: + return readAssetDescriptorOperation(dec) + case tagAssetOperationProof: + return readAssetOperationProof(dec) + case tagAssetOperationOwnershipProof: + return readAssetOperationOwnershipProof(dec) + case tagAssetOperationOwnershipProofETH: + return readAssetOperationOwnershipProofETH(dec) + default: dec.err = coreerr.E("readVariantElementData", fmt.Sprintf("wire: unsupported variant tag 0x%02x (%d)", tag, tag), nil) return nil @@ -928,3 +1055,178 @@ func readZCOutsRangeProof(dec *Decoder) []byte { raw = append(raw, v...) return raw } + +// --- asset operation readers (HF5) --- + +// readAssetDescriptorOperation reads asset_descriptor_operation (tag 40). +// Wire: ver(uint8) + operation_type(uint8) + opt_asset_id(optional 32-byte hash) +// + opt_descriptor(optional AssetDescriptorBase) + amount_to_emit(uint64 LE) +// + amount_to_burn(uint64 LE) + etc(vector). +func readAssetDescriptorOperation(dec *Decoder) []byte { + var raw []byte + // ver: uint8 + ver := dec.ReadUint8() + if dec.err != nil { + return nil + } + raw = append(raw, ver) + // operation_type: uint8 + opType := dec.ReadUint8() + if dec.err != nil { + return nil + } + raw = append(raw, opType) + // opt_asset_id: optional — uint8 marker, then 32 bytes if present + marker := dec.ReadUint8() + if dec.err != nil { + return nil + } + raw = append(raw, marker) + if marker != 0 { + b := dec.ReadBytes(32) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + } + // opt_descriptor: optional + marker = dec.ReadUint8() + if dec.err != nil { + return nil + } + raw = append(raw, marker) + if marker != 0 { + // AssetDescriptorBase: ticker(string) + full_name(string) + // + total_max_supply(uint64 LE) + current_supply(uint64 LE) + // + decimal_point(uint8) + meta_info(string) + owner_key(32) + // + etc(vector) + for range 2 { + s := readStringBlob(dec) + if dec.err != nil { + return nil + } + raw = append(raw, s...) + } + b := dec.ReadBytes(8 + 8 + 1) // total_max_supply + current_supply + decimal_point + if dec.err != nil { + return nil + } + raw = append(raw, b...) + s := readStringBlob(dec) // meta_info + if dec.err != nil { + return nil + } + raw = append(raw, s...) + b = dec.ReadBytes(32) // owner_key + if dec.err != nil { + return nil + } + raw = append(raw, b...) + v := readVariantVectorFixed(dec, 1) // etc + if dec.err != nil { + return nil + } + raw = append(raw, v...) + } + // amount_to_emit: uint64 LE + b := dec.ReadBytes(8) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + // amount_to_burn: uint64 LE + b = dec.ReadBytes(8) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + // etc: vector + v := readVariantVectorFixed(dec, 1) + if dec.err != nil { + return nil + } + raw = append(raw, v...) + return raw +} + +// readAssetOperationProof reads asset_operation_proof (tag 49). +// Wire: ver(uint8) + gss(generic_schnorr_sig_s = 64 bytes) + asset_id(32 bytes) +// + etc(vector). +func readAssetOperationProof(dec *Decoder) []byte { + var raw []byte + // ver: uint8 + b := dec.ReadBytes(1) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + // gss: generic_schnorr_sig_s (s + c = 64 bytes) + b = dec.ReadBytes(64) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + // asset_id: 32 bytes + b = dec.ReadBytes(32) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + // etc: vector + v := readVariantVectorFixed(dec, 1) + if dec.err != nil { + return nil + } + raw = append(raw, v...) + return raw +} + +// readAssetOperationOwnershipProof reads asset_operation_ownership_proof (tag 50). +// Wire: ver(uint8) + gss(generic_schnorr_sig_s = 64 bytes) + etc(vector). +func readAssetOperationOwnershipProof(dec *Decoder) []byte { + var raw []byte + // ver: uint8 + b := dec.ReadBytes(1) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + // gss: generic_schnorr_sig_s (64 bytes) + b = dec.ReadBytes(64) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + // etc: vector + v := readVariantVectorFixed(dec, 1) + if dec.err != nil { + return nil + } + raw = append(raw, v...) + return raw +} + +// readAssetOperationOwnershipProofETH reads asset_operation_ownership_proof_eth (tag 51). +// Wire: ver(uint8) + eth_sig(65 bytes: r=32 + s=32 + v=1) + etc(vector). +func readAssetOperationOwnershipProofETH(dec *Decoder) []byte { + var raw []byte + // ver: uint8 + b := dec.ReadBytes(1) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + // eth_sig: 65 bytes + b = dec.ReadBytes(65) + if dec.err != nil { + return nil + } + raw = append(raw, b...) + // etc: vector + v := readVariantVectorFixed(dec, 1) + if dec.err != nil { + return nil + } + raw = append(raw, v...) + return raw +} -- 2.45.3