go-blockchain/wallet/extra.go

147 lines
4.1 KiB
Go
Raw Permalink Normal View History

// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// You may obtain a copy of the licence at:
//
// https://joinup.ec.europa.eu/software/page/eupl/licence-eupl
//
// SPDX-License-Identifier: EUPL-1.2
package wallet
import (
"encoding/binary"
"fmt"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/blockchain/types"
"dappco.re/go/core/blockchain/wire"
)
// Extra field tag constants from the CryptoNote variant vector encoding.
const (
extraTagDerivationHint = 11 // string-encoded: varint(length) + bytes
extraTagUnlockTime = 14 // varint-encoded
extraTagPublicKey = 22 // fixed 32 bytes
)
// TxExtra holds wallet-critical fields parsed from a transaction's raw extra
// bytes. The Raw field preserves the original bytes for round-tripping.
type TxExtra struct {
TxPublicKey types.PublicKey
UnlockTime uint64
DerivationHint uint16
Raw []byte
}
// ParseTxExtra decodes a CryptoNote variant vector (raw tx extra bytes) and
// extracts the wallet-critical fields: tx public key (tag 22), unlock time
// (tag 14), and derivation hint (tag 11). Unknown tags are skipped.
func ParseTxExtra(raw []byte) (*TxExtra, error) {
extra := &TxExtra{Raw: make([]byte, len(raw))}
copy(extra.Raw, raw)
if len(raw) == 0 {
return extra, nil
}
count, n, err := wire.DecodeVarint(raw)
if err != nil {
return extra, coreerr.E("ParseTxExtra", "wallet: extra: invalid varint count", err)
}
pos := n
for i := uint64(0); i < count && pos < len(raw); i++ {
tag := raw[pos]
pos++
switch tag {
case extraTagPublicKey:
if pos+32 > len(raw) {
return extra, coreerr.E("ParseTxExtra", fmt.Sprintf("wallet: extra: truncated public key at offset %d", pos), nil)
}
copy(extra.TxPublicKey[:], raw[pos:pos+32])
pos += 32
case extraTagUnlockTime:
val, vn, vErr := wire.DecodeVarint(raw[pos:])
if vErr != nil {
return extra, coreerr.E("ParseTxExtra", "wallet: extra: invalid unlock_time varint", vErr)
}
extra.UnlockTime = val
pos += vn
case extraTagDerivationHint:
length, vn, vErr := wire.DecodeVarint(raw[pos:])
if vErr != nil {
return extra, coreerr.E("ParseTxExtra", "wallet: extra: invalid hint length varint", vErr)
}
pos += vn
if length == 2 && pos+2 <= len(raw) {
extra.DerivationHint = binary.LittleEndian.Uint16(raw[pos : pos+2])
}
pos += int(length)
default:
skip, skipErr := skipExtraElement(raw[pos:], tag)
if skipErr != nil {
// Unknown or malformed element; stop parsing but return what we have.
return extra, nil
}
pos += skip
}
}
return extra, nil
}
// BuildTxExtra constructs a minimal raw extra containing only the tx public
// key (tag 22). This is the minimum required for a valid transaction.
func BuildTxExtra(txPubKey types.PublicKey) []byte {
raw := wire.EncodeVarint(1)
raw = append(raw, extraTagPublicKey)
raw = append(raw, txPubKey[:]...)
return raw
}
// skipExtraElement returns the number of data bytes to skip for a given tag,
// based on the CryptoNote variant vector element sizes.
func skipExtraElement(data []byte, tag uint8) (int, error) {
switch tag {
// String types: varint(length) + length bytes.
case 7, 9, 11, 19:
if len(data) == 0 {
return 0, coreerr.E("skipExtraElement", fmt.Sprintf("wallet: extra: no data for string tag %d", tag), nil)
}
length, n, err := wire.DecodeVarint(data)
if err != nil {
return 0, coreerr.E("skipExtraElement", fmt.Sprintf("wallet: extra: invalid string length for tag %d", tag), err)
}
return n + int(length), nil
// Varint types: single varint value.
case 14, 15, 16, 26, 27:
_, n, err := wire.DecodeVarint(data)
if err != nil {
return 0, coreerr.E("skipExtraElement", fmt.Sprintf("wallet: extra: invalid varint for tag %d", tag), err)
}
return n, nil
// Fixed-size types.
case 10:
return 8, nil // 8 bytes
case 17, 28:
return 4, nil // 4 bytes
case 23, 24:
return 2, nil // 2 bytes
case 22:
return 32, nil // public key
case 8, 29:
return 64, nil // signature
default:
return 0, coreerr.E("skipExtraElement", fmt.Sprintf("wallet: extra: unknown tag %d", tag), nil)
}
}