From cc4d39cfbfd87485fb27115c150b371ef00313b3 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Wed, 22 Jul 2026 16:21:01 +0400 Subject: [PATCH 01/16] feat: implement rlp encode/decode --- crypto/rlp.go | 256 +++++++++++++++++++++++++++++++++++++++++++++ crypto/rlp_test.go | 232 ++++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 2 + 4 files changed, 491 insertions(+), 1 deletion(-) create mode 100644 crypto/rlp.go create mode 100644 crypto/rlp_test.go diff --git a/crypto/rlp.go b/crypto/rlp.go new file mode 100644 index 0000000..f79a36c --- /dev/null +++ b/crypto/rlp.go @@ -0,0 +1,256 @@ +package crypto + +import ( + "errors" + "math/big" +) + +var ( + ErrRlpUnsupportedType = errors.New("rlp: unsupported type") + ErrRlpUnexpectedEndOfData = errors.New("rlp: unexpected end of data") + ErrRlpTooLarge = errors.New("rlp: value too large") + ErrRlpNegativeBigInt = errors.New("rlp: cannot encode a negative big.Int") +) + +const ( + rlpStringOffset = 0x80 + rlpListOffset = 0xc0 + rlpSingleByteMax = 0x7f + rlpShortStringMax = 0xb7 + rlpLongStringMax = 0xbf + rlpShortListMax = 0xf7 +) + +// RlpItem is a value that can be RLP-encoded and decoded. +// +// https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/ +type RlpItem interface { + EncodeRLP() ([]byte, error) + DecodeRLP(data []byte) (int, error) +} + +// RlpEncode encodes an RlpItem as RLP. +func RlpEncode(item RlpItem) ([]byte, error) { + return item.EncodeRLP() +} + +// RlpDecode decodes RLP-encoded data into item and returns the number of +// bytes read. The given data may be longer than the encoded item, in which +// case the remaining data is ignored. +func RlpDecode(data []byte, item RlpItem) (int, error) { + return item.DecodeRLP(data) +} + +// RlpBytes is an RLP byte string. +type RlpBytes []byte + +func (s *RlpBytes) EncodeRLP() ([]byte, error) { + if len(*s) == 1 && (*s)[0] <= rlpSingleByteMax { + return []byte{(*s)[0]}, nil + } + + prefix, err := rlpEncodePrefix(len(*s), rlpStringOffset) + if err != nil { + return nil, err + } + + return append(prefix, *s...), nil +} + +func (s *RlpBytes) DecodeRLP(data []byte) (int, error) { + offset, dataLen, prefixLen, err := rlpDecodePrefix(data) + if err != nil { + return 0, err + } + if offset != rlpStringOffset { + return 0, ErrRlpUnsupportedType + } + if uint64(len(data)) < uint64(prefixLen)+dataLen { + return 0, ErrRlpUnexpectedEndOfData + } + + *s = append([]byte{}, data[uint64(prefixLen):uint64(prefixLen)+dataLen]...) + + return prefixLen + int(dataLen), nil +} + +// RlpBigInt is an RLP-encoded arbitrary-precision, non-negative integer. +// Zero always encodes as an empty RLP string. +type RlpBigInt struct{ X *big.Int } + +func NewRlpBigInt(x *big.Int) *RlpBigInt { + return &RlpBigInt{X: x} +} + +func (b *RlpBigInt) EncodeRLP() ([]byte, error) { + if b.X == nil || b.X.Sign() == 0 { + empty := RlpBytes{} + return empty.EncodeRLP() + } + if b.X.Sign() < 0 { + return nil, ErrRlpNegativeBigInt + } + + bytes := RlpBytes(b.X.Bytes()) + return bytes.EncodeRLP() +} + +func (b *RlpBigInt) DecodeRLP(data []byte) (int, error) { + s := RlpBytes{} + + n, err := s.DecodeRLP(data) + if err != nil { + return 0, err + } + + b.X = new(big.Int).SetBytes(s) + + return n, nil +} + +// RlpList is an ordered RLP list of items. When decoding, positions already +// populated with an item are decoded into that item's concrete type; any +// items beyond the pre-populated length are appended as raw RlpBytes. +type RlpList []RlpItem + +func NewRlpList(items ...RlpItem) *RlpList { + list := RlpList(items) + return &list +} + +func (l *RlpList) EncodeRLP() ([]byte, error) { + payload := []byte{} + + for _, item := range *l { + encoded, err := item.EncodeRLP() + if err != nil { + return nil, err + } + payload = append(payload, encoded...) + } + + prefix, err := rlpEncodePrefix(len(payload), rlpListOffset) + if err != nil { + return nil, err + } + + return append(prefix, payload...), nil +} + +func (l *RlpList) DecodeRLP(data []byte) (int, error) { + offset, dataLen, prefixLen, err := rlpDecodePrefix(data) + if err != nil { + return 0, err + } + if offset != rlpListOffset { + return 0, ErrRlpUnsupportedType + } + if uint64(len(data)) < uint64(prefixLen)+dataLen { + return 0, ErrRlpUnexpectedEndOfData + } + + body := data[uint64(prefixLen) : uint64(prefixLen)+dataLen] + + for i := 0; len(body) > 0; i++ { + var item RlpItem + if i < len(*l) { + item = (*l)[i] + } else { + item = &RlpBytes{} + *l = append(*l, item) + } + + consumed, err := item.DecodeRLP(body) + if err != nil { + return 0, err + } + + body = body[consumed:] + } + + return prefixLen + int(dataLen), nil +} + +// rlpEncodePrefix encodes the RLP type-and-length prefix for offset +// (rlpStringOffset or rlpListOffset) and the given payload length. +func rlpEncodePrefix(length int, offset byte) ([]byte, error) { + if length <= 55 { + return []byte{offset + byte(length)}, nil + } + + lengthBytes := rlpEncodeLength(uint64(length)) + if len(lengthBytes) > 8 { + return nil, ErrRlpTooLarge + } + + prefix := make([]byte, 0, 1+len(lengthBytes)) + prefix = append(prefix, offset+55+byte(len(lengthBytes))) + prefix = append(prefix, lengthBytes...) + + return prefix, nil +} + +// rlpEncodeLength returns the minimal big-endian encoding of length. +func rlpEncodeLength(length uint64) []byte { + var buf [8]byte + + n := 0 + for length > 0 { + buf[7-n] = byte(length) + length >>= 8 + n++ + } + + return append([]byte{}, buf[8-n:]...) +} + +// rlpDecodePrefix decodes the RLP type-and-length prefix at the start of +// data, returning which offset (rlpStringOffset or rlpListOffset) applies, +// the payload length, and the prefix length in bytes. +func rlpDecodePrefix(data []byte) (offset byte, dataLen uint64, prefixLen int, err error) { + if len(data) == 0 { + return 0, 0, 0, ErrRlpUnexpectedEndOfData + } + + cur := data[0] + + switch { + case cur <= rlpSingleByteMax: + return rlpStringOffset, 1, 0, nil + case cur <= rlpShortStringMax: + return rlpStringOffset, uint64(cur - rlpStringOffset), 1, nil + case cur <= rlpLongStringMax: + lengthLen := int(cur - rlpShortStringMax) + length, err := rlpReadUint(data[1:], lengthLen) + if err != nil { + return 0, 0, 0, err + } + return rlpStringOffset, length, 1 + lengthLen, nil + case cur <= rlpShortListMax: + return rlpListOffset, uint64(cur - rlpListOffset), 1, nil + default: + lengthLen := int(cur - rlpShortListMax) + length, err := rlpReadUint(data[1:], lengthLen) + if err != nil { + return 0, 0, 0, err + } + return rlpListOffset, length, 1 + lengthLen, nil + } +} + +// rlpReadUint reads a big-endian unsigned integer of the given byte length. +func rlpReadUint(data []byte, length int) (uint64, error) { + if length > 8 { + return 0, ErrRlpTooLarge + } + if len(data) < length { + return 0, ErrRlpUnexpectedEndOfData + } + + var result uint64 + for i := 0; i < length; i++ { + result = (result << 8) | uint64(data[i]) + } + + return result, nil +} diff --git a/crypto/rlp_test.go b/crypto/rlp_test.go new file mode 100644 index 0000000..0a4249a --- /dev/null +++ b/crypto/rlp_test.go @@ -0,0 +1,232 @@ +package crypto + +import ( + "encoding/hex" + "math/big" + "testing" + + "github.com/stretchr/testify/assert" +) + +// newRlpBytes is a test helper: RlpBytes must always be used as *RlpBytes to +// satisfy RlpItem, and a Go type conversion (RlpBytes(b)) is not addressable, +// so tests go through a named variable instead. +func newRlpBytes(b []byte) *RlpBytes { + v := RlpBytes(b) + return &v +} + +func TestRlpEncodeString(t *testing.T) { + assert := assert.New(t) + + encode := func(b []byte) string { + encoded, err := RlpEncode(newRlpBytes(b)) + assert.NoError(err) + return hex.EncodeToString(encoded) + } + + assert.Equal("8774657374696e67", encode([]byte("testing"))) + assert.Equal("80", encode([]byte{})) + assert.Equal("12", encode([]byte{0x12})) + assert.Equal("821212", encode([]byte{0x12, 0x12})) +} + +func TestRlpEncodeList(t *testing.T) { + assert := assert.New(t) + + encoded, err := RlpEncode(NewRlpList(newRlpBytes([]byte("testing")))) + assert.NoError(err) + assert.Equal("c88774657374696e67", hex.EncodeToString(encoded)) +} + +func TestRlpEncodeLongPayload(t *testing.T) { + assert := assert.New(t) + + // 56 bytes forces the "long string"/"long list" branches (>55 bytes). + long := make([]byte, 56) + for i := range long { + long[i] = byte('a') + } + + encoded, err := RlpEncode(newRlpBytes(long)) + assert.NoError(err) + assert.Equal(byte(0xb8), encoded[0]) + assert.Equal(byte(56), encoded[1]) + assert.Equal(long, encoded[2:]) + + listEncoded, err := RlpEncode(NewRlpList(newRlpBytes(long))) + assert.NoError(err) + assert.Equal(byte(0xf8), listEncoded[0]) +} + +func TestRlpEncodeBigInt(t *testing.T) { + assert := assert.New(t) + + encode := func(x *big.Int) string { + encoded, err := RlpEncode(NewRlpBigInt(x)) + assert.NoError(err) + return hex.EncodeToString(encoded) + } + + assert.Equal("80", encode(big.NewInt(0))) + assert.Equal("0a", encode(big.NewInt(10))) + + big256, _ := new(big.Int).SetString("256", 10) + assert.Equal("820100", encode(big256)) +} + +func TestRlpEncodeNegativeBigIntErrors(t *testing.T) { + assert := assert.New(t) + + _, err := RlpEncode(NewRlpBigInt(big.NewInt(-1))) + assert.ErrorIs(err, ErrRlpNegativeBigInt) +} + +func TestRlpDecodeBytesRoundTrip(t *testing.T) { + assert := assert.New(t) + + cases := [][]byte{ + []byte("testing"), + {}, + {0x12}, + {0x12, 0x12}, + } + + for _, c := range cases { + encoded, err := RlpEncode(newRlpBytes(c)) + assert.NoError(err) + + var decoded RlpBytes + n, err := RlpDecode(encoded, &decoded) + assert.NoError(err) + assert.Equal(len(encoded), n) + assert.Equal(RlpBytes(c), decoded) + } +} + +func TestRlpDecodeBigIntRoundTrip(t *testing.T) { + assert := assert.New(t) + + values := []*big.Int{big.NewInt(0), big.NewInt(10), big.NewInt(1_000_000)} + + for _, v := range values { + encoded, err := RlpEncode(NewRlpBigInt(v)) + assert.NoError(err) + + decoded := &RlpBigInt{} + _, err = RlpDecode(encoded, decoded) + assert.NoError(err) + assert.Equal(0, v.Cmp(decoded.X)) + } +} + +func TestRlpDecodeListRoundTrip(t *testing.T) { + assert := assert.New(t) + + list := NewRlpList(newRlpBytes([]byte("testing")), &RlpBytes{0x01}, &RlpBytes{}) + encoded, err := RlpEncode(list) + assert.NoError(err) + + decoded := NewRlpList(&RlpBytes{}, &RlpBytes{}, &RlpBytes{}) + _, err = RlpDecode(encoded, decoded) + assert.NoError(err) + + assert.Equal(RlpBytes("testing"), *(*decoded)[0].(*RlpBytes)) + assert.Equal(RlpBytes{0x01}, *(*decoded)[1].(*RlpBytes)) + assert.Equal(RlpBytes{}, *(*decoded)[2].(*RlpBytes)) +} + +// TestRlpDecodeTransactionShapedList mirrors the Mainsail transaction envelope +// [nonce, gasPrice, gasLimit, to, value, data, v, r, s], decoding directly +// into a pre-typed schema the way serializer/deserializer will. +func TestRlpDecodeTransactionShapedList(t *testing.T) { + assert := assert.New(t) + + to := make([]byte, 20) + for i := range to { + to[i] = byte(i + 1) + } + r := make([]byte, 32) + for i := range r { + r[i] = byte(i + 1) + } + s := make([]byte, 32) + for i := range s { + s[i] = byte(i + 2) + } + + list := NewRlpList( + NewRlpBigInt(big.NewInt(1)), // nonce + NewRlpBigInt(big.NewInt(5)), // gasPrice + NewRlpBigInt(big.NewInt(1_000_000)), // gasLimit + newRlpBytes(to), // to + NewRlpBigInt(big.NewInt(0)), // value + &RlpBytes{}, // data + NewRlpBigInt(big.NewInt(23659)), // v + newRlpBytes(r), // r + newRlpBytes(s), // s + ) + + encoded, err := RlpEncode(list) + assert.NoError(err) + + decoded := NewRlpList( + &RlpBigInt{}, &RlpBigInt{}, &RlpBigInt{}, + &RlpBytes{}, &RlpBigInt{}, &RlpBytes{}, + &RlpBigInt{}, &RlpBytes{}, &RlpBytes{}, + ) + _, err = RlpDecode(encoded, decoded) + assert.NoError(err) + + assert.Equal(9, len(*decoded)) + assert.Equal(RlpBytes(to), *(*decoded)[3].(*RlpBytes)) + assert.Equal(RlpBytes(r), *(*decoded)[7].(*RlpBytes)) + assert.Equal(RlpBytes(s), *(*decoded)[8].(*RlpBytes)) + assert.Equal(0, big.NewInt(1_000_000).Cmp((*decoded)[2].(*RlpBigInt).X)) +} + +// TestRlpDecodeListAppendsExtraItems confirms a list decoded into fewer +// pre-typed slots than are present appends the rest as raw RlpBytes — used to +// detect the optional legacySecondSignature 10th field. +func TestRlpDecodeListAppendsExtraItems(t *testing.T) { + assert := assert.New(t) + + list := NewRlpList(&RlpBytes{0x01}, &RlpBytes{0x02}, &RlpBytes{0x03}) + encoded, err := RlpEncode(list) + assert.NoError(err) + + decoded := NewRlpList(&RlpBytes{}, &RlpBytes{}) + _, err = RlpDecode(encoded, decoded) + assert.NoError(err) + + assert.Equal(3, len(*decoded)) + assert.Equal(RlpBytes{0x03}, *(*decoded)[2].(*RlpBytes)) +} + +func TestRlpDecodeEmptyDataError(t *testing.T) { + assert := assert.New(t) + + var decoded RlpBytes + _, err := RlpDecode([]byte{}, &decoded) + assert.ErrorIs(err, ErrRlpUnexpectedEndOfData) +} + +func TestRlpDecodeTruncatedDataError(t *testing.T) { + assert := assert.New(t) + + // Short-string prefix claiming 7 bytes but only 3 are present. + var decoded RlpBytes + _, err := RlpDecode([]byte{0x87, 0x01, 0x02, 0x03}, &decoded) + assert.ErrorIs(err, ErrRlpUnexpectedEndOfData) +} + +func TestRlpDecodeWrongTypeError(t *testing.T) { + assert := assert.New(t) + + encoded, err := RlpEncode(NewRlpList(newRlpBytes([]byte("x")))) + assert.NoError(err) + + var decoded RlpBytes + _, err = RlpDecode(encoded, &decoded) + assert.ErrorIs(err, ErrRlpUnsupportedType) +} diff --git a/go.mod b/go.mod index ae12fa8..01aae6b 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/ellemouton/schnorr v0.0.0-20230301092540-7b5fdc085456 github.com/fatih/structs v1.1.0 github.com/stretchr/testify v1.9.0 - github.com/supranational/blst v0.3.13 + github.com/supranational/blst v0.3.16 github.com/tyler-smith/go-bip39 v1.1.0 golang.org/x/crypto v0.27.0 ) diff --git a/go.sum b/go.sum index d8d62ef..9deba95 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk= github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= From a7b9f2ce381ea85323804e42a9ea0ca0f1e36fe2 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Wed, 22 Jul 2026 17:12:48 +0400 Subject: [PATCH 02/16] feat: implement abi encode/decode --- crypto/abi.go | 358 ++++++++++++++++++++++++++++++++++++++++++++ crypto/abi_test.go | 218 +++++++++++++++++++++++++++ crypto/contracts.go | 25 ++++ 3 files changed, 601 insertions(+) create mode 100644 crypto/abi.go create mode 100644 crypto/abi_test.go create mode 100644 crypto/contracts.go diff --git a/crypto/abi.go b/crypto/abi.go new file mode 100644 index 0000000..472715c --- /dev/null +++ b/crypto/abi.go @@ -0,0 +1,358 @@ +package crypto + +import ( + "bytes" + "encoding/hex" + "errors" + "math/big" + "strings" + + "golang.org/x/crypto/sha3" +) + +const abiWordLength = 32 +const abiSelectorLength = 4 +const abiAddressLength = 20 + +var ( + ErrAbiInvalidAddress = errors.New("abi: invalid address") + ErrAbiValueTooLarge = errors.New("abi: value exceeds one word (32 bytes)") + ErrAbiNegativeUint = errors.New("abi: uint256 must be non-negative") + ErrAbiUnexpectedEndOfData = errors.New("abi: unexpected end of data") + ErrAbiSelectorMismatch = errors.New("abi: function selector does not match") + ErrAbiInvalidOffset = errors.New("abi: dynamic value offset is invalid") +) + +// AbiFunctionSelector returns the first 4 bytes of keccak256(signature), e.g. +// AbiFunctionSelector("vote(address)"). +func AbiFunctionSelector(signature string) []byte { + hash := sha3.NewLegacyKeccak256() + hash.Write([]byte(signature)) + return hash.Sum(nil)[:abiSelectorLength] +} + +// AbiArg is a single already-ABI-encoded function argument, tagged with +// whether it belongs in the head (static) or tail (dynamic) section of a +// function call's calldata. +type AbiArg struct { + Encoded []byte + Dynamic bool +} + +// AbiEncodeFunctionCall assembles a full function call: the 4-byte selector +// followed by the head/tail encoding of args, per the Solidity ABI spec — +// static args are encoded directly in the head, dynamic args contribute a +// 32-byte offset in the head and their data in the tail. +func AbiEncodeFunctionCall(signature string, args ...AbiArg) []byte { + head := make([]byte, 0, len(args)*abiWordLength) + tail := []byte{} + headLen := len(args) * abiWordLength + + for _, arg := range args { + if arg.Dynamic { + offset := headLen + len(tail) + head = append(head, abiEncodeUintWord(big.NewInt(int64(offset)))...) + tail = append(tail, arg.Encoded...) + } else { + head = append(head, arg.Encoded...) + } + } + + result := make([]byte, 0, abiSelectorLength+len(head)+len(tail)) + result = append(result, AbiFunctionSelector(signature)...) + result = append(result, head...) + result = append(result, tail...) + + return result +} + +// AbiAddress encodes a static "address" argument. address must be a +// "0x"-prefixed, 40-hex-char string. +func AbiAddress(address string) (AbiArg, error) { + encoded, err := abiEncodeAddress(address) + if err != nil { + return AbiArg{}, err + } + return AbiArg{Encoded: encoded, Dynamic: false}, nil +} + +// AbiUint256 encodes a static "uint256" argument. +func AbiUint256(x *big.Int) (AbiArg, error) { + if x == nil || x.Sign() < 0 { + return AbiArg{}, ErrAbiNegativeUint + } + return AbiArg{Encoded: abiEncodeUintWord(x), Dynamic: false}, nil +} + +// AbiBytes encodes a dynamic "bytes" argument. +func AbiBytes(data []byte) AbiArg { + return AbiArg{Encoded: abiEncodeDynamicBytes(data), Dynamic: true} +} + +// AbiString encodes a dynamic "string" argument. +func AbiString(s string) AbiArg { + return AbiArg{Encoded: abiEncodeDynamicBytes([]byte(s)), Dynamic: true} +} + +// AbiAddressArray encodes a dynamic "address[]" argument. +func AbiAddressArray(addresses []string) (AbiArg, error) { + body := make([]byte, 0, len(addresses)*abiWordLength) + + for _, address := range addresses { + word, err := abiEncodeAddress(address) + if err != nil { + return AbiArg{}, err + } + body = append(body, word...) + } + + encoded := append(abiEncodeUintWord(big.NewInt(int64(len(addresses)))), body...) + + return AbiArg{Encoded: encoded, Dynamic: true}, nil +} + +// AbiUint256Array encodes a dynamic "uint256[]" argument. +func AbiUint256Array(values []*big.Int) (AbiArg, error) { + body := make([]byte, 0, len(values)*abiWordLength) + + for _, v := range values { + if v == nil || v.Sign() < 0 { + return AbiArg{}, ErrAbiNegativeUint + } + body = append(body, abiEncodeUintWord(v)...) + } + + encoded := append(abiEncodeUintWord(big.NewInt(int64(len(values)))), body...) + + return AbiArg{Encoded: encoded, Dynamic: true}, nil +} + +func abiEncodeUintWord(x *big.Int) []byte { + word := make([]byte, abiWordLength) + b := x.Bytes() + copy(word[abiWordLength-len(b):], b) + return word +} + +func abiEncodeAddress(address string) ([]byte, error) { + addressBytes, err := abiDecodeAddressBytes(address) + if err != nil { + return nil, err + } + + word := make([]byte, abiWordLength) + copy(word[abiWordLength-abiAddressLength:], addressBytes) + + return word, nil +} + +func abiDecodeAddressBytes(address string) ([]byte, error) { + if !strings.HasPrefix(address, "0x") || len(address) != 2+abiAddressLength*2 { + return nil, ErrAbiInvalidAddress + } + + addressBytes, err := hex.DecodeString(address[2:]) + if err != nil { + return nil, ErrAbiInvalidAddress + } + + return addressBytes, nil +} + +func abiEncodeDynamicBytes(data []byte) []byte { + lengthWord := abiEncodeUintWord(big.NewInt(int64(len(data)))) + + paddedLen := len(data) + if rem := paddedLen % abiWordLength; rem != 0 { + paddedLen += abiWordLength - rem + } + + body := make([]byte, paddedLen) + copy(body, data) + + return append(lengthWord, body...) +} + +// AbiDecoder decodes the calldata of a single, known function call: the +// 4-byte selector followed by a flat sequence of 32-byte head words, one per +// top-level argument, where dynamic arguments' head word is an offset +// pointing into the tail section. +type AbiDecoder struct { + head [][]byte + tail []byte +} + +// NewAbiDecoder validates that data starts with the given function selector +// and splits the remaining calldata into its head words and tail region, +// where argCount is the function's total number of top-level arguments. +func NewAbiDecoder(data []byte, signature string, argCount int) (*AbiDecoder, error) { + if len(data) < abiSelectorLength { + return nil, ErrAbiUnexpectedEndOfData + } + if !bytes.Equal(data[:abiSelectorLength], AbiFunctionSelector(signature)) { + return nil, ErrAbiSelectorMismatch + } + + body := data[abiSelectorLength:] + if len(body) < argCount*abiWordLength { + return nil, ErrAbiUnexpectedEndOfData + } + + head := make([][]byte, argCount) + for i := 0; i < argCount; i++ { + head[i] = body[i*abiWordLength : (i+1)*abiWordLength] + } + + return &AbiDecoder{head: head, tail: body}, nil +} + +// Address decodes the argIndex-th argument as a static "address". +func (d *AbiDecoder) Address(argIndex int) (string, error) { + word, err := d.headWord(argIndex) + if err != nil { + return "", err + } + + addressBytes := word[abiWordLength-abiAddressLength:] + address := "0x" + EIP55Checksum(hex.EncodeToString(addressBytes)) + + return address, nil +} + +// Uint256 decodes the argIndex-th argument as a static "uint256". +func (d *AbiDecoder) Uint256(argIndex int) (*big.Int, error) { + word, err := d.headWord(argIndex) + if err != nil { + return nil, err + } + + return new(big.Int).SetBytes(word), nil +} + +// Bytes decodes the argIndex-th argument as a dynamic "bytes". +func (d *AbiDecoder) Bytes(argIndex int) ([]byte, error) { + tailData, err := d.dynamicTail(argIndex) + if err != nil { + return nil, err + } + if len(tailData) < abiWordLength { + return nil, ErrAbiUnexpectedEndOfData + } + + lengthWord := tailData[:abiWordLength] + tailData = tailData[abiWordLength:] + + length, err := abiWordToBoundedInt(lengthWord, len(tailData)) + if err != nil { + return nil, err + } + + return append([]byte{}, tailData[:length]...), nil +} + +// String decodes the argIndex-th argument as a dynamic "string". +func (d *AbiDecoder) String(argIndex int) (string, error) { + data, err := d.Bytes(argIndex) + if err != nil { + return "", err + } + return string(data), nil +} + +// AddressArray decodes the argIndex-th argument as a dynamic "address[]". +func (d *AbiDecoder) AddressArray(argIndex int) ([]string, error) { + tailData, err := d.dynamicTail(argIndex) + if err != nil { + return nil, err + } + + count, elements, err := abiArrayElements(tailData) + if err != nil { + return nil, err + } + + addresses := make([]string, count) + for i := 0; i < count; i++ { + word := elements[i*abiWordLength : (i+1)*abiWordLength] + addresses[i] = "0x" + EIP55Checksum(hex.EncodeToString(word[abiWordLength-abiAddressLength:])) + } + + return addresses, nil +} + +// Uint256Array decodes the argIndex-th argument as a dynamic "uint256[]". +func (d *AbiDecoder) Uint256Array(argIndex int) ([]*big.Int, error) { + tailData, err := d.dynamicTail(argIndex) + if err != nil { + return nil, err + } + + count, elements, err := abiArrayElements(tailData) + if err != nil { + return nil, err + } + + values := make([]*big.Int, count) + for i := 0; i < count; i++ { + values[i] = new(big.Int).SetBytes(elements[i*abiWordLength : (i+1)*abiWordLength]) + } + + return values, nil +} + +func abiArrayElements(tailData []byte) (int, []byte, error) { + if len(tailData) < abiWordLength { + return 0, nil, ErrAbiUnexpectedEndOfData + } + + elements := tailData[abiWordLength:] + + count, err := abiWordToBoundedInt(tailData[:abiWordLength], len(elements)/abiWordLength) + if err != nil { + return 0, nil, err + } + + return count, elements, nil +} + +func (d *AbiDecoder) headWord(argIndex int) ([]byte, error) { + if argIndex < 0 || argIndex >= len(d.head) { + return nil, ErrAbiUnexpectedEndOfData + } + return d.head[argIndex], nil +} + +// dynamicTail follows the offset stored in the argIndex-th head word and +// returns the tail data starting at that offset. +func (d *AbiDecoder) dynamicTail(argIndex int) ([]byte, error) { + word, err := d.headWord(argIndex) + if err != nil { + return nil, err + } + + offset, err := abiWordToBoundedInt(word, len(d.tail)) + if err != nil { + return nil, ErrAbiInvalidOffset + } + + return d.tail[offset:], nil +} + +// abiWordToBoundedInt reads a 32-byte ABI word as a length/offset/count value, +// rejecting it unless it is non-negative and no greater than maxLen. This +// guards against a malformed or adversarial word causing big.Int.Int64() +// overflow, or a value that would later be used as an allocation size or +// slice bound before the available data has been confirmed to support it. +func abiWordToBoundedInt(word []byte, maxLen int) (int, error) { + v := new(big.Int).SetBytes(word) + if !v.IsInt64() { + return 0, ErrAbiInvalidOffset + } + + n := v.Int64() + if n < 0 || n > int64(maxLen) { + return 0, ErrAbiInvalidOffset + } + + return int(n), nil +} diff --git a/crypto/abi_test.go b/crypto/abi_test.go new file mode 100644 index 0000000..6d87ee3 --- /dev/null +++ b/crypto/abi_test.go @@ -0,0 +1,218 @@ +package crypto + +import ( + "encoding/hex" + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// testAddress builds a syntactically valid "0x"-prefixed 20-byte address with +// the given last byte, avoiding hand-typed hex strings (easy to get the wrong +// length by miscounting zeros). +func testAddress(lastByte byte) string { + b := make([]byte, abiAddressLength) + b[abiAddressLength-1] = lastByte + return "0x" + hex.EncodeToString(b) +} + +// TestAbiFunctionSelectorKnownVectors checks against the two most widely used +// ERC-20 function selectors, which are independently verifiable (they appear +// in essentially every Ethereum tool/wallet/block explorer), not just +// self-consistent with our own encoder. +func TestAbiFunctionSelectorKnownVectors(t *testing.T) { + assert := assert.New(t) + + assert.Equal("a9059cbb", hex.EncodeToString(AbiFunctionSelector("transfer(address,uint256)"))) + assert.Equal("095ea7b3", hex.EncodeToString(AbiFunctionSelector("approve(address,uint256)"))) +} + +// TestAbiEncodeFunctionCallKnownVector hand-verifies the byte layout of a +// simple static-only call against the ABI spec's head-only encoding rule (no +// dynamic args means no offsets, no tail — just the selector followed by +// left-padded words in argument order). +func TestAbiEncodeFunctionCallKnownVector(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + address := testAddress(0x01) + addressArg, err := AbiAddress(address) + require.NoError(err) + + amountArg, err := AbiUint256(big.NewInt(1000)) + require.NoError(err) + + encoded := AbiEncodeFunctionCall("transfer(address,uint256)", addressArg, amountArg) + + expected := "a9059cbb" + + "0000000000000000000000000000000000000000000000000000000000000001" + + "00000000000000000000000000000000000000000000000000000000000003e8" + assert.Equal(expected, hex.EncodeToString(encoded)) +} + +func TestAbiEncodeDecodeAddressRoundTrip(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + address := testAddress(0x01) + arg, err := AbiAddress(address) + require.NoError(err) + + encoded := AbiEncodeFunctionCall("vote(address)", arg) + + decoder, err := NewAbiDecoder(encoded, "vote(address)", 1) + require.NoError(err) + + decodedAddress, err := decoder.Address(0) + require.NoError(err) + assert.Equal(address, decodedAddress) +} + +func TestAbiEncodeDecodeUint256RoundTrip(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + values := []*big.Int{big.NewInt(0), big.NewInt(1), big.NewInt(1_000_000_000)} + + for _, v := range values { + arg, err := AbiUint256(v) + require.NoError(err) + + encoded := AbiEncodeFunctionCall("approve(address,uint256)", AbiArg{Encoded: make([]byte, abiWordLength)}, arg) + + decoder, err := NewAbiDecoder(encoded, "approve(address,uint256)", 2) + require.NoError(err) + + decoded, err := decoder.Uint256(1) + require.NoError(err) + assert.Equal(0, v.Cmp(decoded)) + } +} + +func TestAbiEncodeDecodeBytesRoundTrip(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + cases := [][]byte{ + {}, + {0x01}, + []byte("a value longer than one 32-byte word to force padding math"), + } + + for _, c := range cases { + arg := AbiBytes(c) + full := AbiEncodeFunctionCall("registerValidator(bytes,bytes)", arg, AbiBytes([]byte{})) + + decoder, err := NewAbiDecoder(full, "registerValidator(bytes,bytes)", 2) + require.NoError(err) + + decoded, err := decoder.Bytes(0) + require.NoError(err) + assert.Equal(c, decoded) + } +} + +func TestAbiEncodeDecodeStringRoundTrip(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + username := "simple_tx_tester" + encoded := AbiEncodeFunctionCall("registerUsername(string)", AbiString(username)) + + decoder, err := NewAbiDecoder(encoded, "registerUsername(string)", 1) + require.NoError(err) + + decoded, err := decoder.String(0) + require.NoError(err) + assert.Equal(username, decoded) +} + +func TestAbiEncodeDecodeAddressArrayRoundTrip(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + addresses := []string{testAddress(0x01), testAddress(0x02)} + amounts := []*big.Int{big.NewInt(100), big.NewInt(200)} + + addressesArg, err := AbiAddressArray(addresses) + require.NoError(err) + amountsArg, err := AbiUint256Array(amounts) + require.NoError(err) + + encoded := AbiEncodeFunctionCall("pay(address[],uint256[])", addressesArg, amountsArg) + + decoder, err := NewAbiDecoder(encoded, "pay(address[],uint256[])", 2) + require.NoError(err) + + decodedAddresses, err := decoder.AddressArray(0) + require.NoError(err) + assert.Equal(addresses, decodedAddresses) + + decodedAmounts, err := decoder.Uint256Array(1) + require.NoError(err) + assert.Equal(len(amounts), len(decodedAmounts)) + for i, a := range amounts { + assert.Equal(0, a.Cmp(decodedAmounts[i])) + } +} + +func TestAbiEncodeInvalidAddressErrors(t *testing.T) { + assert := assert.New(t) + + _, err := AbiAddress("not-an-address") + assert.ErrorIs(err, ErrAbiInvalidAddress) + + _, err = AbiAddress("0x01") // too short + assert.ErrorIs(err, ErrAbiInvalidAddress) +} + +func TestAbiEncodeNegativeUintErrors(t *testing.T) { + assert := assert.New(t) + + _, err := AbiUint256(big.NewInt(-1)) + assert.ErrorIs(err, ErrAbiNegativeUint) + + _, err = AbiUint256Array([]*big.Int{big.NewInt(1), big.NewInt(-1)}) + assert.ErrorIs(err, ErrAbiNegativeUint) +} + +func TestAbiDecodeSelectorMismatchErrors(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + arg, err := AbiAddress(testAddress(0x01)) + require.NoError(err) + encoded := AbiEncodeFunctionCall("vote(address)", arg) + + _, err = NewAbiDecoder(encoded, "unvote()", 0) + assert.ErrorIs(err, ErrAbiSelectorMismatch) +} + +func TestAbiDecodeTruncatedDataErrors(t *testing.T) { + assert := assert.New(t) + + selector := AbiFunctionSelector("vote(address)") + + _, err := NewAbiDecoder(selector, "vote(address)", 1) // selector only, no head word + assert.ErrorIs(err, ErrAbiUnexpectedEndOfData) +} + +func TestAbiDecodeInvalidOffsetErrors(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + selector := AbiFunctionSelector("registerUsername(string)") + // Head word claims an offset far beyond any data that actually follows. + badOffset := make([]byte, abiWordLength) + badOffset[abiWordLength-1] = 0xff + + data := append(append([]byte{}, selector...), badOffset...) + + decoder, err := NewAbiDecoder(data, "registerUsername(string)", 1) + require.NoError(err) + + _, err = decoder.String(0) + assert.ErrorIs(err, ErrAbiInvalidOffset) +} diff --git a/crypto/contracts.go b/crypto/contracts.go new file mode 100644 index 0000000..046cffc --- /dev/null +++ b/crypto/contracts.go @@ -0,0 +1,25 @@ +package crypto + +// Well-known Mainsail system contract addresses. +const ( + ContractConsensus = "0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1" + ContractMultipayment = "0x00EFd0D4639191C49908A7BddbB9A11A994A8527" + ContractUsernames = "0x2c1DE3b4Dbb4aDebEbB5dcECAe825bE2a9fc6eb6" + ContractBatchTransfer = "0x5a223F4434D5Bd8478100EEb3b0166a57A26350d" +) + +// ABI function signatures for the Mainsail system contracts, plus the +// generic ERC-20 functions used by the token convenience builders. +const ( + AbiSignatureVote = "vote(address)" + AbiSignatureUnvote = "unvote()" + AbiSignatureRegisterValidator = "registerValidator(bytes,bytes)" + AbiSignatureResignValidator = "resignValidator()" + AbiSignatureUpdateValidator = "updateValidator(bytes,bytes)" + AbiSignatureRegisterUsername = "registerUsername(string)" + AbiSignatureResignUsername = "resignUsername()" + AbiSignatureMultipayment = "pay(address[],uint256[])" + AbiSignatureErc20Transfer = "transfer(address,uint256)" + AbiSignatureErc20Approve = "approve(address,uint256)" + AbiSignatureErc20BatchTransferFrom = "batchTransferFrom(address,address[],uint256[])" +) From fc317315d1c84b2d8cf04f772848557f8287c408 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Wed, 22 Jul 2026 17:23:21 +0400 Subject: [PATCH 03/16] feat: implement abi encode/decode --- go.mod | 6 +++--- go.sum | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 01aae6b..33e20e1 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/arkEcosystem/go-crypto -go 1.22.4 +go 1.25.0 require ( github.com/btcsuite/btcd v0.20.1-beta @@ -10,12 +10,12 @@ require ( github.com/stretchr/testify v1.9.0 github.com/supranational/blst v0.3.16 github.com/tyler-smith/go-bip39 v1.1.0 - golang.org/x/crypto v0.27.0 + golang.org/x/crypto v0.54.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.25.0 // indirect + golang.org/x/sys v0.47.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9deba95..692535f 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,8 @@ golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -50,6 +52,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 6de71057246760142b23eaa06784f1af05f7450b Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Thu, 23 Jul 2026 13:40:59 +0400 Subject: [PATCH 04/16] feat: implement abi encode/decode --- crypto/abi.go | 70 +++++--- crypto/abi_test.go | 28 +++ crypto/address.go | 31 +++- crypto/builder.go | 218 ++--------------------- crypto/builder_test.go | 323 ---------------------------------- crypto/configuration.go | 27 +-- crypto/contracts.go | 6 +- crypto/deserializer.go | 277 +++++++---------------------- crypto/enums.go | 40 ----- crypto/fixtures.go | 8 - crypto/fixtures/identity.json | 2 +- crypto/message.go | 37 ++-- crypto/message_test.go | 60 ------- crypto/networks.go | 11 +- crypto/private_key.go | 123 +++++++------ crypto/private_key_test.go | 1 - crypto/public_key.go | 61 ++++--- crypto/rlp.go | 11 +- crypto/rlp_test.go | 16 ++ crypto/serdeser_test.go | 71 -------- crypto/serializer.go | 206 ++++++---------------- crypto/structs.go | 125 +++---------- crypto/transaction.go | 233 ++++++++---------------- go.mod | 5 +- go.sum | 17 +- 25 files changed, 485 insertions(+), 1522 deletions(-) delete mode 100644 crypto/builder_test.go delete mode 100644 crypto/enums.go delete mode 100644 crypto/message_test.go delete mode 100644 crypto/serdeser_test.go diff --git a/crypto/abi.go b/crypto/abi.go index 472715c..088c887 100644 --- a/crypto/abi.go +++ b/crypto/abi.go @@ -2,10 +2,8 @@ package crypto import ( "bytes" - "encoding/hex" "errors" "math/big" - "strings" "golang.org/x/crypto/sha3" ) @@ -51,7 +49,7 @@ func AbiEncodeFunctionCall(signature string, args ...AbiArg) []byte { for _, arg := range args { if arg.Dynamic { offset := headLen + len(tail) - head = append(head, abiEncodeUintWord(big.NewInt(int64(offset)))...) + head = append(head, abiEncodeSmallUintWord(offset)...) tail = append(tail, arg.Encoded...) } else { head = append(head, arg.Encoded...) @@ -81,7 +79,13 @@ func AbiUint256(x *big.Int) (AbiArg, error) { if x == nil || x.Sign() < 0 { return AbiArg{}, ErrAbiNegativeUint } - return AbiArg{Encoded: abiEncodeUintWord(x), Dynamic: false}, nil + + encoded, err := abiEncodeUintWord(x) + if err != nil { + return AbiArg{}, err + } + + return AbiArg{Encoded: encoded, Dynamic: false}, nil } // AbiBytes encodes a dynamic "bytes" argument. @@ -106,7 +110,7 @@ func AbiAddressArray(addresses []string) (AbiArg, error) { body = append(body, word...) } - encoded := append(abiEncodeUintWord(big.NewInt(int64(len(addresses)))), body...) + encoded := append(abiEncodeSmallUintWord(len(addresses)), body...) return AbiArg{Encoded: encoded, Dynamic: true}, nil } @@ -119,48 +123,59 @@ func AbiUint256Array(values []*big.Int) (AbiArg, error) { if v == nil || v.Sign() < 0 { return AbiArg{}, ErrAbiNegativeUint } - body = append(body, abiEncodeUintWord(v)...) + + word, err := abiEncodeUintWord(v) + if err != nil { + return AbiArg{}, err + } + body = append(body, word...) } - encoded := append(abiEncodeUintWord(big.NewInt(int64(len(values)))), body...) + encoded := append(abiEncodeSmallUintWord(len(values)), body...) return AbiArg{Encoded: encoded, Dynamic: true}, nil } -func abiEncodeUintWord(x *big.Int) []byte { +// abiPadWordLeft left-pads b into a 32-byte word. Callers must ensure +// len(b) <= abiWordLength. +func abiPadWordLeft(b []byte) []byte { word := make([]byte, abiWordLength) - b := x.Bytes() copy(word[abiWordLength-len(b):], b) return word } -func abiEncodeAddress(address string) ([]byte, error) { - addressBytes, err := abiDecodeAddressBytes(address) - if err != nil { - return nil, err +// abiEncodeUintWord encodes an arbitrary, possibly caller-supplied uint256, +// rejecting values that don't fit in one 32-byte word. +func abiEncodeUintWord(x *big.Int) ([]byte, error) { + b := x.Bytes() + if len(b) > abiWordLength { + return nil, ErrAbiValueTooLarge } - word := make([]byte, abiWordLength) - copy(word[abiWordLength-abiAddressLength:], addressBytes) - - return word, nil + return abiPadWordLeft(b), nil } -func abiDecodeAddressBytes(address string) ([]byte, error) { - if !strings.HasPrefix(address, "0x") || len(address) != 2+abiAddressLength*2 { - return nil, ErrAbiInvalidAddress - } +// abiEncodeSmallUintWord encodes a non-negative, internally-computed +// offset/length/count. Safe by construction, not just in practice: an int64's +// big-endian representation is at most 8 bytes, always well under the +// 32-byte word size, so there is no failure mode to check for. +func abiEncodeSmallUintWord(n int) []byte { + return abiPadWordLeft(big.NewInt(int64(n)).Bytes()) +} - addressBytes, err := hex.DecodeString(address[2:]) +func abiEncodeAddress(address string) ([]byte, error) { + addressBytes, err := AddressToBytes(address) if err != nil { return nil, ErrAbiInvalidAddress } - return addressBytes, nil + word := abiPadWordLeft(addressBytes) + + return word, nil } func abiEncodeDynamicBytes(data []byte) []byte { - lengthWord := abiEncodeUintWord(big.NewInt(int64(len(data)))) + lengthWord := abiEncodeSmallUintWord(len(data)) paddedLen := len(data) if rem := paddedLen % abiWordLength; rem != 0 { @@ -213,10 +228,7 @@ func (d *AbiDecoder) Address(argIndex int) (string, error) { return "", err } - addressBytes := word[abiWordLength-abiAddressLength:] - address := "0x" + EIP55Checksum(hex.EncodeToString(addressBytes)) - - return address, nil + return AddressFromBytes(word[abiWordLength-abiAddressLength:]), nil } // Uint256 decodes the argIndex-th argument as a static "uint256". @@ -274,7 +286,7 @@ func (d *AbiDecoder) AddressArray(argIndex int) ([]string, error) { addresses := make([]string, count) for i := 0; i < count; i++ { word := elements[i*abiWordLength : (i+1)*abiWordLength] - addresses[i] = "0x" + EIP55Checksum(hex.EncodeToString(word[abiWordLength-abiAddressLength:])) + addresses[i] = AddressFromBytes(word[abiWordLength-abiAddressLength:]) } return addresses, nil diff --git a/crypto/abi_test.go b/crypto/abi_test.go index 6d87ee3..6a5610e 100644 --- a/crypto/abi_test.go +++ b/crypto/abi_test.go @@ -158,6 +158,34 @@ func TestAbiEncodeDecodeAddressArrayRoundTrip(t *testing.T) { } } +// TestAbiEncodeDecodeEmptyArrayRoundTrip exercises the zero-element case, +// where the array's length prefix is abiEncodeSmallUintWord(0) — the exact +// edge case at the center of the abiEncodeSmallUintWord/abiEncodeUintWord +// split (a zero-length big.Int encodes as an empty byte slice, not a +// single zero byte). +func TestAbiEncodeDecodeEmptyArrayRoundTrip(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + addressesArg, err := AbiAddressArray([]string{}) + require.NoError(err) + amountsArg, err := AbiUint256Array([]*big.Int{}) + require.NoError(err) + + encoded := AbiEncodeFunctionCall("pay(address[],uint256[])", addressesArg, amountsArg) + + decoder, err := NewAbiDecoder(encoded, "pay(address[],uint256[])", 2) + require.NoError(err) + + decodedAddresses, err := decoder.AddressArray(0) + require.NoError(err) + assert.Equal(0, len(decodedAddresses)) + + decodedAmounts, err := decoder.Uint256Array(1) + require.NoError(err) + assert.Equal(0, len(decodedAmounts)) +} + func TestAbiEncodeInvalidAddressErrors(t *testing.T) { assert := assert.New(t) diff --git a/crypto/address.go b/crypto/address.go index 9e2dc4a..92b7c68 100644 --- a/crypto/address.go +++ b/crypto/address.go @@ -13,6 +13,10 @@ import ( "strings" ) +const AddressByteLength = 20 + +var ErrInvalidAddress = errors.New("invalid address format") + func AddressFromPassphrase(passphrase string) (string, error) { privateKey, err := PrivateKeyFromPassphrase(passphrase) if err != nil { @@ -21,13 +25,28 @@ func AddressFromPassphrase(passphrase string) (string, error) { return privateKey.ToAddress(), nil } -func ValidateAddress(address string) (bool, error) { - if !strings.HasPrefix(address, "0x") || len(address) != 42 { - return false, errors.New("invalid address format") +// AddressToBytes decodes a "0x"-prefixed, 40-hex-char address into its raw +// 20 bytes. +func AddressToBytes(address string) ([]byte, error) { + if !strings.HasPrefix(address, "0x") || len(address) != 2+AddressByteLength*2 { + return nil, ErrInvalidAddress } - _, err := hex.DecodeString(address[2:]) + + addressBytes, err := hex.DecodeString(address[2:]) if err != nil { - return false, err + return nil, ErrInvalidAddress } - return true, nil + + return addressBytes, nil +} + +// AddressFromBytes formats raw 20 address bytes as a "0x"-prefixed, +// EIP-55-checksummed address. +func AddressFromBytes(addressBytes []byte) string { + return "0x" + EIP55Checksum(hex.EncodeToString(addressBytes)) +} + +func ValidateAddress(address string) (bool, error) { + _, err := AddressToBytes(address) + return err == nil, err } diff --git a/crypto/builder.go b/crypto/builder.go index 3f43b2e..2eb82be 100644 --- a/crypto/builder.go +++ b/crypto/builder.go @@ -8,211 +8,30 @@ package crypto import ( - "errors" - "encoding/hex" + "errors" + "math/big" blst "github.com/supranational/blst/bindings/go" ) -func buildSignedTransaction(transaction *Transaction, passphrase string, secondPassphrase string) *Transaction { - transaction.Sign(passphrase) - - if len(secondPassphrase) > 0 { - transaction.SecondSign(secondPassphrase) - } - - transaction.Id = transaction.GetId() - - return transaction -} - -func buildMultiSignedTransaction(transaction *Transaction, signerIndex int, passphrase string) *Transaction { - transaction.SignMulti(signerIndex, passphrase) - - transaction.Id = transaction.GetId() - - return transaction -} - -func setCommonFields(transaction *Transaction, transactionType uint16) { - if transaction.Fee == 0 { - transaction.Fee = GetFee(transactionType) - } - - if transaction.Network == 0 { - transaction.Network = GetNetwork().Version - } - - transaction.SecondSenderPublicKey = "" - transaction.SecondSignature = "" - - if transaction.Timestamp == 0 { - transaction.Timestamp = GetTime() - } - - transaction.Type = transactionType - transaction.TypeGroup = TRANSACTION_TYPE_GROUPS.Core - transaction.Version = 1 -} - -/** Set all fields and sign a TransactionTypes.Transfer transaction. - * Members of the supplied transaction that must be set when calling this function: - * Amount - * Expiration - optional, could be 0 to designate no expiration - * Fee - optional, if 0, then it will be set to a default fee - * Network - optional, if 0, then it will be set to the configured network - * Nonce - * RecipientId - * Timestamp - optional, if 0, then it will be set to the present time - * VendorField - optional */ -func BuildTransfer(transaction *Transaction, passphrase string, secondPassphrase string) *Transaction { - setCommonFields(transaction, TRANSACTION_TYPES.Transfer) - - transaction.Asset = &TransactionAsset{} - - return buildSignedTransaction(transaction, passphrase, secondPassphrase) -} - -/** Set all fields and sign a multi signature TransactionTypes.Transfer transaction. - * Members of the supplied transaction that must be set when calling this function: - * Amount - * Expiration - optional, could be 0 to designate no expiration - * Fee - optional, if 0, then it will be set to a default fee - * Network - optional, if 0, then it will be set to the configured network - * Nonce - * RecipientId - * Signatures - must be an array (could be empty); a new signature will be appended to it - * Timestamp - optional, if 0, then it will be set to the present time - * VendorField - optional */ -func BuildTransferMultiSignature(transaction *Transaction, signerIndex int, passphrase string) *Transaction { - setCommonFields(transaction, TRANSACTION_TYPES.Transfer) - - transaction.Asset = &TransactionAsset{} - - return buildMultiSignedTransaction(transaction, signerIndex, passphrase) -} - -/** Set all fields and sign a TransactionTypes.ValidatorRegistration transaction. - * Members of the supplied transaction that must be set when calling this function: - * Asset.Delegate.Username - * Expiration - optional, could be 0 to designate no expiration - * Fee - optional, if 0, then it will be set to a default fee - * Network - optional, if 0, then it will be set to the configured network - * Nonce - * Timestamp - optional, if 0, then it will be set to the present time - * VendorField - optional */ -func BuildValidatorRegistration(transaction *Transaction, passphrase string, secondPassphrase string) *Transaction { - setCommonFields(transaction, TRANSACTION_TYPES.ValidatorRegistration) - - if transaction.Asset != nil && transaction.Asset.Validator != nil { - err := validateBLSPublicKey(transaction.Asset.Validator.ValidatorPublicKey) - if err != nil { - panic("Invalid BLS public key: " + err.Error()) - } - } - - return buildSignedTransaction(transaction, passphrase, secondPassphrase) -} - -/** Set all fields and sign a TransactionTypes.Vote transaction. - * Members of the supplied transaction that must be set when calling this function: - * Asset.Votes - * Expiration - optional, could be 0 to designate no expiration - * Fee - optional, if 0, then it will be set to a default fee - * Network - optional, if 0, then it will be set to the configured network - * Nonce - * Timestamp - optional, if 0, then it will be set to the present time - * VendorField - optional */ -func BuildVote(transaction *Transaction, passphrase string, secondPassphrase string) *Transaction { - setCommonFields(transaction, TRANSACTION_TYPES.Vote) - - transaction.RecipientId, _ = AddressFromPassphrase(passphrase) - - return buildSignedTransaction(transaction, passphrase, secondPassphrase) -} - -/** Set all fields and sign a TransactionTypes.MultiSignatureRegistration transaction. - * Members of the supplied transaction that must be set when calling this function: - * Asset.MultiSignature - * Expiration - optional, could be 0 to designate no expiration - * Fee - optional, if 0, then it will be set to a default fee - * Network - optional, if 0, then it will be set to the configured network - * Nonce - * Timestamp - optional, if 0, then it will be set to the present time - * VendorField - optional */ -func BuildMultiSignatureRegistration(transaction *Transaction, passphrase string, secondPassphrase string) *Transaction { - setCommonFields(transaction, TRANSACTION_TYPES.MultiSignatureRegistration) - - return buildSignedTransaction(transaction, passphrase, secondPassphrase) -} - -/** Set all fields and sign a TransactionTypes.MultiPayment transaction. - * Members of the supplied transaction that must be set when calling this function: - * Asset.Payments - * Expiration - optional, could be 0 to designate no expiration - * Fee - optional, if 0, then it will be set to a default fee - * Network - optional, if 0, then it will be set to the configured network - * Nonce - * Timestamp - optional, if 0, then it will be set to the present time - * VendorField - optional */ -func BuildMultiPayment(transaction *Transaction, passphrase string, secondPassphrase string) *Transaction { - setCommonFields(transaction, TRANSACTION_TYPES.MultiPayment) - - return buildSignedTransaction(transaction, passphrase, secondPassphrase) -} - -/** Set all fields and sign a TransactionTypes.ValidatorResignation transaction. - * Members of the supplied transaction that must be set when calling this function: - * Expiration - optional, could be 0 to designate no expiration - * Fee - optional, if 0, then it will be set to a default fee - * Network - optional, if 0, then it will be set to the configured network - * Nonce - * Timestamp - optional, if 0, then it will be set to the present time - * VendorField - optional */ -func BuildValidatorResignation(transaction *Transaction, passphrase string, secondPassphrase string) *Transaction { - setCommonFields(transaction, TRANSACTION_TYPES.ValidatorResignation) - - return buildSignedTransaction(transaction, passphrase, secondPassphrase) -} - - -/** Set all fields and sign a TransactionTypes.UsernameRegistration transaction. - * Members of the supplied transaction that must be set when calling this function: - * Asset.Username.Username - * Expiration - optional, could be 0 to designate no expiration - * Fee - optional, if 0, then it will be set to a default fee - * Network - optional, if 0, then it will be set to the configured network - * Nonce - * Timestamp - optional, if 0, then it will be set to the present time - * VendorField - optional */ - func BuildUsernameRegistration(transaction *Transaction, passphrase string, secondPassphrase string) *Transaction { - setCommonFields(transaction, TRANSACTION_TYPES.UsernameRegistration) +// Default gas parameters used by NewTransaction, matching php-crypto/ +// typescript-crypto's AbstractTransactionBuilder defaults. +var ( + DefaultGasPrice = big.NewInt(5) + DefaultGasLimit = big.NewInt(1_000_000) +) - // Validate if Username is set - if transaction.Asset != nil && transaction.Asset.Username != nil { - if transaction.Asset.Username.Username == "" { - panic("Invalid username: username is empty") - } - } else { - panic("Invalid username: no username asset provided") +// NewTransaction returns a Transaction with the default nonce/gasPrice/ +// gasLimit/value builder defaults set. Concrete transaction-type builders +// (BuildTransfer, BuildVote, etc.) are layered on top of this. +func NewTransaction() *Transaction { + return &Transaction{ + Nonce: big.NewInt(1), + GasPrice: DefaultGasPrice, + GasLimit: DefaultGasLimit, + Value: big.NewInt(0), } - - return buildSignedTransaction(transaction, passphrase, secondPassphrase) -} - -/** Set all fields and sign a TransactionTypes.UsernameResignation transaction. - * Members of the supplied transaction that must be set when calling this function: - * Expiration - optional, could be 0 to designate no expiration - * Fee - optional, if 0, then it will be set to a default fee - * Network - optional, if 0, then it will be set to the configured network - * Nonce - * Timestamp - optional, if 0, then it will be set to the present time - * VendorField - optional */ -func BuildUsernameResignation(transaction *Transaction, passphrase string, secondPassphrase string) *Transaction { - setCommonFields(transaction, TRANSACTION_TYPES.UsernameResignation) - - return buildSignedTransaction(transaction, passphrase, secondPassphrase) } func validateBLSPublicKey(publicKey string) error { @@ -220,17 +39,14 @@ func validateBLSPublicKey(publicKey string) error { return errors.New("invalid BLS public key length") } - // Decode the public key from hex pubKeyBytes, err := hex.DecodeString(publicKey) if err != nil { return errors.New("invalid BLS public key hex format") } - // Deserialize the public key into a blst.P1Affine structure var pubKey blst.P1Affine pubKey.Deserialize(pubKeyBytes) - // Check if the public key is in G1 group and is valid if !pubKey.InG1() { return errors.New("invalid BLS public key: not in G1 group or invalid structure") } diff --git a/crypto/builder_test.go b/crypto/builder_test.go deleted file mode 100644 index 9775c04..0000000 --- a/crypto/builder_test.go +++ /dev/null @@ -1,323 +0,0 @@ -// This file is part of Ark Go Crypto. -// -// (c) Ark Ecosystem -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -package crypto - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" -) - -func transferWithPassphrase(t *testing.T) *Transaction { - return BuildTransfer( - &Transaction{ - Amount: FlexToshi(133380000000), - Expiration: 4333222, - Fee: FlexToshi(10), - Network: 30, - Nonce: 6, - RecipientId: "0xb0FF9213f7226bBB72b84dE16af86e56f1f38B01", - }, - "my super secret passphrase", - "", - ) -} - -func transferWithSecondPassphrase(t *testing.T) *Transaction { - secondPassPhrase := "This is a top secret second passphrase" - - transaction := BuildTransfer( - &Transaction{ - Amount: FlexToshi(133380000000), - Nonce: 5, - RecipientId: "0xb0FF9213f7226bBB72b84dE16af86e56f1f38B01", - VendorField: "This is a transaction from Go", - }, - "This is a top secret passphrase", - secondPassPhrase, - ) - - assert := assert.New(t) - - secondPublicKey, _ := PublicKeyFromPassphrase(secondPassPhrase) - assert.True(transaction.SecondVerify(secondPublicKey)) - - return transaction -} - -func transferMultiSignature(t *testing.T) *Transaction { - transaction := &Transaction{ - Amount: FlexToshi(200000000), - Expiration: 4333222, - Fee: FlexToshi(10), - Network: 30, - Nonce: 6, - RecipientId: "0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A", - } - - transaction = BuildTransferMultiSignature(transaction, 0, "multisig participant 1") - transaction = BuildTransferMultiSignature(transaction, 1, "multisig participant 2") - - return transaction -} - -func validatorRegistrationWithPassphrase(t *testing.T) *Transaction { - return BuildValidatorRegistration( - &Transaction{ - Asset: &TransactionAsset{ - Validator: &ValidatorAsset{ - ValidatorPublicKey: "a08058db53e2665c84a40f5152e76dd2b652125a6079130d4c315e728bcf4dd1dfb44ac26e82302331d61977d3141118", - }, - }, - Nonce: 5, - }, - "lumber desk thought industry island man slow vendor pact fragile enact season", - "", - ) -} - -func validatorRegistrationWithSecondPassphrase(t *testing.T) *Transaction { - secondPassPhrase := "This is a top secret second passphrase" - - transaction := BuildValidatorRegistration( - &Transaction{ - Asset: &TransactionAsset{ - Validator: &ValidatorAsset{ - ValidatorPublicKey: "a08058db53e2665c84a40f5152e76dd2b652125a6079130d4c315e728bcf4dd1dfb44ac26e82302331d61977d3141118", - }, - }, - Nonce: 5, - }, - "This is a top secret passphrase", - secondPassPhrase, - ) - - assert := assert.New(t) - - secondPublicKey, _ := PublicKeyFromPassphrase(secondPassPhrase) - assert.True(transaction.SecondVerify(secondPublicKey)) - - return transaction -} - -func usernameRegistrationWithPassphrase(t *testing.T) *Transaction { - return BuildUsernameRegistration( - &Transaction{ - Asset: &TransactionAsset{ - Username: &UsernameAsset{ - Username: "test", - }, - }, - Nonce: 5, - }, - "your secret passphrase", - "", - ) -} - -func usernameResignationWithPassphrase(t *testing.T) *Transaction { - return BuildUsernameResignation( - &Transaction{ - Nonce: 5, - }, - "your secret passphrase", - "", - ) -} - - -func voteWithPassphrase(t *testing.T) *Transaction { - return BuildVote( - &Transaction{ - Asset: &TransactionAsset{ - Votes: []string{"034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed192"}, - }, - Nonce: 5, - }, - "This is a top secret passphrase", - "", - ) -} - -func voteWithSecondPassphrase(t *testing.T) *Transaction { - secondPassPhrase := "This is a top secret second passphrase" - - transaction := BuildVote( - &Transaction{ - Asset: &TransactionAsset{ - Votes: []string{"034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed192"}, - }, - Nonce: 5, - }, - "This is a top secret passphrase", - secondPassPhrase, - ) - - assert := assert.New(t) - - secondPublicKey, _ := PublicKeyFromPassphrase(secondPassPhrase) - assert.True(transaction.SecondVerify(secondPublicKey)) - - return transaction -} - -func unvoteVoteWithPassphrase(t *testing.T) *Transaction { - return BuildVote( - &Transaction{ - Asset: &TransactionAsset{ - Votes: []string{ - "034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed193", - }, - Unvotes: []string{ - "034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed192", - }, - }, - Nonce: 5, - }, - "This is a top secret passphrase", - "", - ) -} - -func multiSignatureRegistrationWithPassphrase(t *testing.T) *Transaction { - return BuildMultiSignatureRegistration( - &Transaction{ - Asset: &TransactionAsset{ - MultiSignature: &MultiSignatureRegistrationAsset{ - Min: 2, - PublicKeys: []string{ - "03a02b9d5fdd1307c2ee4652ba54d492d1fd11a7d1bb3f3a44c4a05e79f19de933", - "03b02b9d5fdd1307c2ee4652ba54d492d1fd11a7d1bb3f3a44c4a05e79f19de933", - "03c02b9d5fdd1307c2ee4652ba54d492d1fd11a7d1bb3f3a44c4a05e79f19de933", - }, - }, - }, - Nonce: 5, - }, - "This is a top secret passphrase", - "", - ) -} - -func multiPaymentWithPassphrase(t *testing.T) *Transaction { - return BuildMultiPayment( - &Transaction{ - Asset: &TransactionAsset{ - Payments: []*MultiPaymentAsset{ - {Amount: FlexToshi(111222), RecipientId: "0xb0FF9213f7226bBB72b84dE16af86e56f1f38B01"}, - {Amount: FlexToshi(222333), RecipientId: "0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A"}, - {Amount: FlexToshi(333444), RecipientId: "0xb0FF9213f7226bBB72b84dE16af86e56f1f38B01"}, - }, - }, - Nonce: 5, - }, - "This is a top secret passphrase", - "", - ) -} - -func validatorResignationWithPassphrase(t *testing.T) *Transaction { - return BuildValidatorResignation( - &Transaction{ - Amount: FlexToshi(0), - Nonce: 5, - }, - "This is a top secret passphrase", - "", - ) -} - - -func TestBuild(t *testing.T) { - for builderName, buildTransaction := range map[string]func(*testing.T) *Transaction{ - "TransferWithPassphrase": transferWithPassphrase, - "TransferWithSecondPassphrase": transferWithSecondPassphrase, - "ValidatorRegistrationWithPassphrase": validatorRegistrationWithPassphrase, - "ValidatorRegistrationWithSecondPassphrase": validatorRegistrationWithSecondPassphrase, - "VoteWithPassphrase": voteWithPassphrase, - "UsernameRegistrationWithPassphrase": usernameRegistrationWithPassphrase, - "UsernameResignationWithPassphrase": usernameResignationWithPassphrase, - "VoteWithSecondPassphrase": voteWithSecondPassphrase, - "UnvoteVoteWithPassphrase": unvoteVoteWithPassphrase, - "MultiSignatureRegistrationWithPassphrase": multiSignatureRegistrationWithPassphrase, - "MultiPaymentWithPassphrase": multiPaymentWithPassphrase, - "ValidatorResignationWithPassphrase": validatorResignationWithPassphrase, - } { - // Iterate only over Schnorr signature type - for signatureTypeString, signatureType := range map[string]int{ - "Schnorr": SIGNATURE_TYPE_SCHNORR, - } { - CONFIG_SIGNATURE_TYPE = signatureType - - test := func(t *testing.T) { - transaction := buildTransaction(t) - - assert := assert.New(t) - - assert.True(transaction.Verify()) - } - - t.Run(fmt.Sprintf("%s-%s", builderName, signatureTypeString), test) - } - } - - // Test multisignature transfer separately - test := func(t *testing.T) { - transaction := transferMultiSignature(t) - - assert := assert.New(t) - - multiSignatureAsset := &MultiSignatureRegistrationAsset{ - Min: 2, - PublicKeys: []string{ - "037eaa8cb236c40a08fcb9d6220743ee6ae1b5c40e8a77a38f286516c3ff663901", - "0301fd417566397113ba8c55de2f093a572744ed1829b37b56a129058000ef7bce", - }, - } - - assert.True(transaction.Verify(multiSignatureAsset)) - } - - t.Run("TransferMultiSignature-Schnorr", test) -} - - -func TestBuildValidatorRegistrationWithInvalidKeyLength(t *testing.T) { - assert.PanicsWithValue(t, "Invalid BLS public key: invalid BLS public key length", func() { - BuildValidatorRegistration( - &Transaction{ - Asset: &TransactionAsset{ - Validator: &ValidatorAsset{ - ValidatorPublicKey: "b08058db53e2665c84a40f5152e76dd2b65212", - }, - }, - Nonce: 5, - }, - "lumber desk thought industry island man slow vendor pact fragile enact season", - "", - ) - }) -} - -func TestBuildValidatorRegistrationWithInvalidKey(t *testing.T) { - assert.PanicsWithValue(t, "Invalid BLS public key: invalid BLS public key hex format", func() { - BuildValidatorRegistration( - &Transaction{ - Asset: &TransactionAsset{ - Validator: &ValidatorAsset{ - ValidatorPublicKey: "j08058db53e2665c84a40f5152e76dd2b652125a6079130d4c315e728bcf4dd1dfb44ac26e82302331d61977d3141118", - }, - }, - Nonce: 5, - }, - "lumber desk thought industry island man slow vendor pact fragile enact season", - "", - ) - }) -} \ No newline at end of file diff --git a/crypto/configuration.go b/crypto/configuration.go index 5a76de5..b4d2c6d 100644 --- a/crypto/configuration.go +++ b/crypto/configuration.go @@ -7,24 +7,11 @@ package crypto -var ( - CONFIG_NETWORK = &Network{} - CONFIG_FEES = map[uint16]FlexToshi{ - TRANSACTION_TYPES.Transfer: TRANSACTION_FEES.Transfer, - TRANSACTION_TYPES.ValidatorRegistration: TRANSACTION_FEES.ValidatorRegistration, - TRANSACTION_TYPES.Vote: TRANSACTION_FEES.Vote, - TRANSACTION_TYPES.MultiSignatureRegistration: TRANSACTION_FEES.MultiSignatureRegistration, - TRANSACTION_TYPES.MultiPayment: TRANSACTION_FEES.MultiPayment, - TRANSACTION_TYPES.ValidatorResignation: TRANSACTION_FEES.ValidatorResignation, - TRANSACTION_TYPES.UsernameRegistration: TRANSACTION_FEES.UsernameRegistration, - TRANSACTION_TYPES.UsernameResignation: TRANSACTION_FEES.UsernameResignation, - } - CONFIG_SIGNATURE_TYPE = SIGNATURE_TYPE_SCHNORR -) +var CONFIG_NETWORK = &Network{} func GetNetwork() *Network { - if CONFIG_NETWORK.Version == 0 { - return NETWORKS_DEVNET + if CONFIG_NETWORK.ChainId == 0 { + return NETWORKS_TESTNET } return CONFIG_NETWORK @@ -33,11 +20,3 @@ func GetNetwork() *Network { func SetNetwork(network *Network) { CONFIG_NETWORK = network } - -func GetFee(transactionType uint16) FlexToshi { - return CONFIG_FEES[transactionType] -} - -func SetFee(transactionType uint16, value FlexToshi) { - CONFIG_FEES[transactionType] = value -} diff --git a/crypto/contracts.go b/crypto/contracts.go index 046cffc..825b1ed 100644 --- a/crypto/contracts.go +++ b/crypto/contracts.go @@ -19,7 +19,7 @@ const ( AbiSignatureRegisterUsername = "registerUsername(string)" AbiSignatureResignUsername = "resignUsername()" AbiSignatureMultipayment = "pay(address[],uint256[])" - AbiSignatureErc20Transfer = "transfer(address,uint256)" - AbiSignatureErc20Approve = "approve(address,uint256)" - AbiSignatureErc20BatchTransferFrom = "batchTransferFrom(address,address[],uint256[])" + AbiSignatureERC20Transfer = "transfer(address,uint256)" + AbiSignatureERC20Approve = "approve(address,uint256)" + AbiSignatureERC20BatchTransferFrom = "batchTransferFrom(address,address[],uint256[])" ) diff --git a/crypto/deserializer.go b/crypto/deserializer.go index 80d938b..aeff9d9 100644 --- a/crypto/deserializer.go +++ b/crypto/deserializer.go @@ -8,240 +8,95 @@ package crypto import ( - "encoding/binary" + "errors" + "math/big" ) -const compactPubKeyLen = 33 // bytes -const addressLen = 20 // bytes - -func deserializeAddress(serialized []byte, offset int) (address string, offsetAfter int) { - if len(serialized[offset:]) >= addressLen { - addressBytes := serialized[offset : offset+addressLen] - address = "0x" + EIP55Checksum(HexEncode(addressBytes)) - offsetAfter = offset + addressLen - } else { - address = "" - offsetAfter = offset - } - return -} - -func DeserializeTransaction(serialized string) *Transaction { - transaction := &Transaction{} - transaction.Serialized = HexDecode(serialized) - - typeSpecificOffset := deserializeHeader(transaction) - transaction = deserializeTypeSpecific(typeSpecificOffset, transaction) - transaction = deserializeCommon(transaction) - - return transaction -} - -//////////////////////////////////////////////////////////////////////////////// -// GENERIC DESERIALISING /////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -func deserializeHeader(transaction *Transaction) int { - transaction.Version = transaction.Serialized[1:2][0] - transaction.Network = transaction.Serialized[2:3][0] - transaction.TypeGroup = binary.LittleEndian.Uint32(transaction.Serialized[3:7]) - transaction.Type = binary.LittleEndian.Uint16(transaction.Serialized[7:9]) - transaction.Nonce = binary.LittleEndian.Uint64(transaction.Serialized[9:17]) - transaction.SenderPublicKey = HexEncode(transaction.Serialized[17:50]) - transaction.Fee = FlexToshi(binary.LittleEndian.Uint64(transaction.Serialized[50:58])) - - vendorFieldLength := transaction.Serialized[58:59][0] +var ( + ErrDeserializeTruncated = errors.New("deserialize: transaction data is truncated") + ErrDeserializeInvalidTo = errors.New("deserialize: \"to\" field is not a valid address length") + ErrDeserializeInvalidV = errors.New("deserialize: v field does not decode to a valid recovery id") +) - if vendorFieldLength > 0 { - transaction.VendorField = string(transaction.Serialized[59:59 + vendorFieldLength]) +// DeserializeTransaction decodes a hex-encoded RLP transaction envelope +// ([nonce, gasPrice, gasLimit, to, value, data, v, r, s]), reverses the +// EIP-155 v encoding back to a raw recovery id, computes the transaction +// hash, and recovers the sender's public key and address from the signature. +func DeserializeTransaction(serializedHex string) (*Transaction, error) { + serialized := HexDecode(serializedHex) + + list := NewRlpList( + &RlpBigInt{}, &RlpBigInt{}, &RlpBigInt{}, + &RlpBytes{}, &RlpBigInt{}, &RlpBytes{}, + &RlpBigInt{}, &RlpBytes{}, &RlpBytes{}, + ) + + if _, err := RlpDecode(serialized, list); err != nil { + return nil, err } - typeSpecificOffset := int(59 + vendorFieldLength) - - return typeSpecificOffset -} - -func deserializeTypeSpecific(typeSpecificOffset int, transaction *Transaction) *Transaction { - switch transaction.Type { - case TRANSACTION_TYPES.Transfer: - transaction = deserializeTransfer(typeSpecificOffset, transaction) - case TRANSACTION_TYPES.ValidatorRegistration: - transaction = deserializeValidatorRegistration(typeSpecificOffset, transaction) - case TRANSACTION_TYPES.Vote: - transaction = deserializeVote(typeSpecificOffset, transaction) - case TRANSACTION_TYPES.MultiSignatureRegistration: - transaction = deserializeMultiSignatureRegistration(typeSpecificOffset, transaction) - case TRANSACTION_TYPES.MultiPayment: - transaction = deserializeMultiPayment(typeSpecificOffset, transaction) - case TRANSACTION_TYPES.ValidatorResignation: - transaction = deserializeValidatorResignation(typeSpecificOffset, transaction) - case TRANSACTION_TYPES.UsernameRegistration: - transaction = deserializeUsernameRegistration(typeSpecificOffset, transaction) - case TRANSACTION_TYPES.UsernameResignation: - transaction = deserializeUsernameResignation(typeSpecificOffset, transaction) + items := *list + if len(items) < 9 { + return nil, ErrDeserializeTruncated } - return transaction -} - -func deserializeCommon(transaction *Transaction) *Transaction { - if transaction.Id == "" { - transaction.Id = transaction.GetId() + toBytes := []byte(*items[3].(*RlpBytes)) + if len(toBytes) != 0 && len(toBytes) != AddressByteLength { + return nil, ErrDeserializeInvalidTo } - return transaction -} - -//////////////////////////////////////////////////////////////////////////////// -// TYPE SPECIFIC DESERIALISING ///////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -func deserializeTransfer(typeSpecificOffset int, transaction *Transaction) *Transaction { - o := typeSpecificOffset - - transaction.Amount = FlexToshi(binary.LittleEndian.Uint64(transaction.Serialized[o : o+8])) - o += 8 - - transaction.Expiration = binary.LittleEndian.Uint32(transaction.Serialized[o : o+4]) - o += 4 - - address, newOffset := deserializeAddress(transaction.Serialized, o) - - transaction.RecipientId = address - o = newOffset - - return transaction.ParseSignatures(o) -} - -func deserializeUsernameRegistration(typeSpecificOffset int, transaction *Transaction) *Transaction { - o := typeSpecificOffset - - usernameLength := int(transaction.Serialized[o]) - o++ - - username := string(transaction.Serialized[o : o+usernameLength]) - o += usernameLength - - transaction.Asset = &TransactionAsset{ - Username: &UsernameAsset{ - Username: username, - }, + transaction := &Transaction{ + Nonce: items[0].(*RlpBigInt).X, + GasPrice: items[1].(*RlpBigInt).X, + GasLimit: items[2].(*RlpBigInt).X, + Value: items[4].(*RlpBigInt).X, + Data: []byte(*items[5].(*RlpBytes)), + Serialized: serialized, } - return transaction.ParseSignatures(o) -} - - -func deserializeValidatorRegistration(typeSpecificOffset int, transaction *Transaction) *Transaction { - o := typeSpecificOffset - - publicKeyLength := 48 - - - transaction.Asset = &TransactionAsset{ - Validator: &ValidatorAsset{ - ValidatorPublicKey: HexEncode(transaction.Serialized[o:o + publicKeyLength]), - }, + if len(toBytes) > 0 { + transaction.To = AddressFromBytes(toBytes) } - o += publicKeyLength - return transaction.ParseSignatures(o) -} - -func deserializeVote(typeSpecificOffset int, transaction *Transaction) *Transaction { - o := typeSpecificOffset - - // Read the number of votes - numVotes := int(transaction.Serialized[o]) - o++ - - transaction.Asset = &TransactionAsset{} - transaction.Asset.Votes = make([]string, 0, numVotes) - - // Read the votes - for i := 0; i < numVotes; i++ { - voteBytes := transaction.Serialized[o : o+compactPubKeyLen] - o += compactPubKeyLen - - transaction.Asset.Votes = append(transaction.Asset.Votes, HexEncode(voteBytes)) + chainId := big.NewInt(int64(GetNetwork().ChainId)) + recoveryId, err := recoveryIdFromEip155V(items[6].(*RlpBigInt).X, chainId) + if err != nil { + return nil, err } + transaction.V = recoveryId + transaction.R = padCurveBytes([]byte(*items[7].(*RlpBytes))) + transaction.S = padCurveBytes([]byte(*items[8].(*RlpBytes))) - // Read the number of unvotes - numUnvotes := int(transaction.Serialized[o]) - o++ - - transaction.Asset.Unvotes = make([]string, 0, numUnvotes) - - // Read the unvotes - for i := 0; i < numUnvotes; i++ { - unvoteBytes := transaction.Serialized[o : o+compactPubKeyLen] - o += compactPubKeyLen - - transaction.Asset.Unvotes = append(transaction.Asset.Unvotes, HexEncode(unvoteBytes)) + hash, err := transaction.GetHash() + if err != nil { + return nil, err } + transaction.Hash = hash - return transaction.ParseSignatures(o) -} - - -func deserializeMultiSignatureRegistration(typeSpecificOffset int, transaction *Transaction) *Transaction { - o := typeSpecificOffset - - transaction.Asset = &TransactionAsset{ - MultiSignature: &MultiSignatureRegistrationAsset{ - Min: transaction.Serialized[o], - }, + if err := transaction.RecoverSender(); err != nil { + return nil, err } - o++ - - count := int(transaction.Serialized[o]) - o++ - for i := 0; i < count; i++ { - keyHex := HexEncode(transaction.Serialized[o:o + compactPubKeyLen]) - o += compactPubKeyLen - - transaction.Asset.MultiSignature.PublicKeys = - append(transaction.Asset.MultiSignature.PublicKeys, keyHex) - } - - return transaction.ParseSignatures(o) + return transaction, nil } -func deserializeMultiPayment(typeSpecificOffset int, transaction *Transaction) *Transaction { - o := typeSpecificOffset - - numRecipients := binary.LittleEndian.Uint16(transaction.Serialized[o:o + 2]) - o += 2 - - transaction.Asset = &TransactionAsset{} - - for i := uint16(0); i < numRecipients; i++ { - payment := &MultiPaymentAsset{} - - payment.Amount = FlexToshi(binary.LittleEndian.Uint64(transaction.Serialized[o:o + 8])) - o += 8 - - payment.RecipientId, o = deserializeAddress(transaction.Serialized, o) - - transaction.Asset.Payments = append(transaction.Asset.Payments, payment) +// recoveryIdFromEip155V reverses EIP-155's v = recoveryId + chainId*2 + 35, +// rejecting the result unless it is a valid ECDSA recovery id (0-3). This +// guards against a malformed or adversarial v field producing an out-of-range +// or unrepresentable value that would otherwise silently corrupt or panic on +// the big.Int-to-int conversion. +func recoveryIdFromEip155V(vField, chainId *big.Int) (int, error) { + recoveryId := new(big.Int).Sub(vField, new(big.Int).Mul(chainId, big.NewInt(2))) + recoveryId.Sub(recoveryId, big.NewInt(35)) + + if !recoveryId.IsInt64() { + return 0, ErrDeserializeInvalidV } - var sum uint64 - - for _, payment := range transaction.Asset.Payments { - sum += uint64(payment.Amount) + n := recoveryId.Int64() + if n < 0 || n > 3 { + return 0, ErrDeserializeInvalidV } - - transaction.Amount = FlexToshi(sum) - return transaction.ParseSignatures(o) + return int(n), nil } - -func deserializeValidatorResignation(typeSpecificOffset int, transaction *Transaction) *Transaction { - return transaction.ParseSignatures(typeSpecificOffset) -} - -func deserializeUsernameResignation(typeSpecificOffset int, transaction *Transaction) *Transaction { - return transaction.ParseSignatures(typeSpecificOffset) -} \ No newline at end of file diff --git a/crypto/enums.go b/crypto/enums.go deleted file mode 100644 index d3a5e3c..0000000 --- a/crypto/enums.go +++ /dev/null @@ -1,40 +0,0 @@ -// This file is part of Ark Go Crypto. -// -// (c) Ark Ecosystem -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -package crypto - -var ( - TRANSACTION_TYPES = &TransactionTypes{ - Transfer: 0, - ValidatorRegistration: 2, - Vote: 3, - MultiSignatureRegistration: 4, - MultiPayment: 6, - ValidatorResignation: 7, - UsernameRegistration: 8, - UsernameResignation: 9, - } - TRANSACTION_TYPE_GROUPS = &TransactionTypeGroups{ - Test: 0, - Core: 1, - } - TRANSACTION_FEES = &TransactionFees{ - Transfer: 10000000, - ValidatorRegistration: 2500000000, - Vote: 100000000, - MultiSignatureRegistration: 500000000, - MultiPayment: 10000000, - ValidatorResignation: 2500000000, - UsernameRegistration: 2500000000, - UsernameResignation: 2500000000, - } -) - -const ( - SIGNATURE_TYPE_ECDSA = 0 - SIGNATURE_TYPE_SCHNORR = 1 -) diff --git a/crypto/fixtures.go b/crypto/fixtures.go index 637740e..b722968 100644 --- a/crypto/fixtures.go +++ b/crypto/fixtures.go @@ -46,8 +46,6 @@ func GetMessageFixture() TestingMessageFixture { return fixture } - - func GetBLSValidatorFixture() BLSValidatorFixture { data := GetFixture("bls_validator") @@ -66,12 +64,6 @@ func GetBLSKeysFixture() []BLSKeyFixture { return fixtures } -type TestingFixture struct { - MultiSignatureAsset MultiSignatureRegistrationAsset `json:"multiSignatureAsset"` - Transaction Transaction `json:"transaction"` - SerializedHex string `json:"serializedHex"` -} - type TestingIdentityFixture struct { Data struct { PrivateKey string `json:"privateKey,omitempty"` diff --git a/crypto/fixtures/identity.json b/crypto/fixtures/identity.json index 662d91b..228d89b 100644 --- a/crypto/fixtures/identity.json +++ b/crypto/fixtures/identity.json @@ -3,7 +3,7 @@ "privateKey": "d8839c2432bfd0a67ef10a804ba991eabba19f154a3d707917681d45822a5712", "publicKey": "034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed192", "address": "0xb0FF9213f7226bBB72b84dE16af86e56f1f38B01", - "wif": "SGq4xLgZKCGxs7bjmwnBrWcT4C1ADFEermj846KC97FSv1WFD1dA" + "wif": "Ue7A6vSx7ewATPp2dA6UbJ8F39DbZwaHTqhD1MrhzmJqRJmvfZ6C" }, "passphrase": "this is a top secret passphrase" } diff --git a/crypto/message.go b/crypto/message.go index a1a604c..56878d0 100644 --- a/crypto/message.go +++ b/crypto/message.go @@ -14,46 +14,43 @@ import ( "github.com/fatih/structs" ) +// SignMessage signs an arbitrary message with the private key derived from +// passphrase. +// +// NOTE: this still hashes the message with plain sha256, carried over +// unchanged from before the Mainsail migration. Ethereum's personal_sign +// convention (keccak256("\x19Ethereum Signed Message:\n" + len(message) + +// message)) is a separate, not-yet-made design change. func SignMessage(message string, passphrase string) (*Message, error) { privateKey, err := PrivateKeyFromPassphrase(passphrase) - - if err != nil { - return nil, err - } - - hash := sha256.New() - _, err = hash.Write([]byte(message)) - if err != nil { return nil, err } - signature, err := privateKey.Sign(hash.Sum(nil)) - - if err != nil { - return nil, err - } + hash := sha256.Sum256([]byte(message)) + sig := privateKey.Sign(hash[:]) return &Message{ PublicKey: HexEncode(privateKey.PublicKey.Serialize()), - Signature: HexEncode(signature), + Signature: HexEncode(sig.Bytes()), Message: message, }, nil } func (message *Message) Verify() (bool, error) { - publicKey, _ := PublicKeyFromBytes(HexDecode(message.PublicKey)) - - hash := sha256.New() - _, err := hash.Write([]byte(message.Message)) + publicKey, err := PublicKeyFromBytes(HexDecode(message.PublicKey)) + if err != nil { + return false, err + } + sig, err := EcdsaSignatureFromBytes(HexDecode(message.Signature)) if err != nil { return false, err } - verified, _ := publicKey.Verify(HexDecode(message.Signature), hash.Sum(nil)) + hash := sha256.Sum256([]byte(message.Message)) - return verified, nil + return publicKey.Verify(hash[:], sig) } func (message *Message) ToMap() map[string]interface{} { diff --git a/crypto/message_test.go b/crypto/message_test.go deleted file mode 100644 index 2b88635..0000000 --- a/crypto/message_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// This file is part of Ark Go Crypto. -// -// (c) Ark Ecosystem -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -package crypto - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSignMessage(t *testing.T) { - CONFIG_SIGNATURE_TYPE = SIGNATURE_TYPE_ECDSA - - fixture := GetMessageFixture() - - message, _ := SignMessage(fixture.Data.Message, fixture.Passphrase) - - assert := assert.New(t) - assert.Equal(fixture.Data.PublicKey, message.PublicKey) - assert.Equal(fixture.Data.Signature, message.Signature) - assert.Equal(fixture.Data.Message, message.Message) -} - -func TestVerifyMessage(t *testing.T) { - fixture := GetMessageFixture() - - message, _ := SignMessage(fixture.Data.Message, fixture.Passphrase) - - assert := assert.New(t) - assert.True(message.Verify()) -} - -func TestMessageToMap(t *testing.T) { - fixture := GetMessageFixture() - - message, _ := SignMessage(fixture.Data.Message, fixture.Passphrase) - - actual := message.ToMap() - expected := map[string]interface{}{"Message": "Hello World", "PublicKey": "034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed192", "Signature": "cdc26c4d137dbbad22ec94fee0bb7d7c1864291aec69c5d30a2d585efb2aa5e349e5497bb94221d8394e56a04e9b39cf9960c73b8212f46d2f03597cd73ebd33"} - - assert := assert.New(t) - assert.EqualValues(expected, actual) -} - -func TestMessageToJson(t *testing.T) { - fixture := GetMessageFixture() - - message, _ := SignMessage(fixture.Data.Message, fixture.Passphrase) - - actual, _ := message.ToJson() - expected := "{\"message\":\"Hello World\",\"publickey\":\"034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed192\",\"signature\":\"cdc26c4d137dbbad22ec94fee0bb7d7c1864291aec69c5d30a2d585efb2aa5e349e5497bb94221d8394e56a04e9b39cf9960c73b8212f46d2f03597cd73ebd33\"}" - - assert := assert.New(t) - assert.Equal(expected, actual) -} diff --git a/crypto/networks.go b/crypto/networks.go index ac95d25..7c1d261 100644 --- a/crypto/networks.go +++ b/crypto/networks.go @@ -12,17 +12,12 @@ import "time" var ( NETWORKS_MAINNET = &Network{ Epoch: time.Date(2017, 3, 21, 13, 00, 0, 0, time.UTC), - Version: 23, - Wif: 170, - } - NETWORKS_DEVNET = &Network{ - Epoch: time.Date(2017, 3, 21, 13, 00, 0, 0, time.UTC), - Version: 30, - Wif: 170, + ChainId: 11811, + Wif: 186, } NETWORKS_TESTNET = &Network{ Epoch: time.Date(2017, 3, 21, 13, 00, 0, 0, time.UTC), - Version: 23, + ChainId: 11812, Wif: 186, } ) diff --git a/crypto/private_key.go b/crypto/private_key.go index 1ac47b7..311dfde 100644 --- a/crypto/private_key.go +++ b/crypto/private_key.go @@ -11,11 +11,24 @@ import ( "crypto/sha256" "fmt" - "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcutil/base58" - "github.com/ellemouton/schnorr" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" ) +const ecdsaCurveByteLength = 32 + +// EcdsaSignature is an ECDSA secp256k1 recoverable signature: R and S are +// each 32-byte big-endian values, and RecoveryId is the raw recovery id +// (0-3) needed to recover the signer's public key from (hash, signature). +// RecoveryId is not yet EIP-155 encoded — that only happens when a +// transaction is serialized. +type EcdsaSignature struct { + R []byte + S []byte + RecoveryId int +} + func PrivateKeyFromPassphrase(passphrase string) (*PrivateKey, error) { hash := sha256.Sum256([]byte(passphrase)) return PrivateKeyFromBytes(hash[:]), nil @@ -26,11 +39,11 @@ func PrivateKeyFromHex(privateKeyHex string) (*PrivateKey, error) { } func PrivateKeyFromBytes(bytes []byte) *PrivateKey { - privateKey, publicKey := btcec.PrivKeyFromBytes(btcec.S256(), bytes) + privateKey := secp256k1.PrivKeyFromBytes(bytes) return &PrivateKey{ PrivateKey: privateKey, PublicKey: &PublicKey{ - PublicKey: publicKey, + PublicKey: privateKey.PubKey(), isCompressed: true, Network: GetNetwork(), }, @@ -59,75 +72,69 @@ func (privateKey *PrivateKey) ToWif() string { return base58.CheckEncode(p, privateKey.PublicKey.Network.Wif) } -//////////////////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////////////////// // CRYPTOGRAPHY //////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////////////////// func (privateKey *PrivateKey) Serialize() []byte { return privateKey.PrivateKey.Serialize() } -func (privateKey *PrivateKey) Sign(hash []byte) ([]byte, error) { - // Parse the private key using the schnorr package - schnorrPrivKey, err := schnorr.ParsePrivKeyHexString(HexEncode(privateKey.PrivateKey.Serialize())) - if err != nil { - return nil, fmt.Errorf("failed to parse Schnorr private key: %v", err) - } - - // Sign the hash using Schnorr - signature, err := schnorrPrivKey.Sign(hash, make([]byte, 32)) - if err != nil { - return nil, fmt.Errorf("failed to sign hash with Schnorr: %v", err) - } - - // Convert [64]byte to []byte - sigArray := signature.Bytes() - sigSlice := make([]byte, 64) - copy(sigSlice, sigArray[:]) +// Sign produces a recoverable ECDSA signature over hash using RFC6979 +// deterministic k and BIP0062 low-S normalization (both handled by +// ecdsa.SignCompact, which cannot fail for a valid key and hash). +func (privateKey *PrivateKey) Sign(hash []byte) *EcdsaSignature { + compact := ecdsa.SignCompact(privateKey.PrivateKey, hash, privateKey.PublicKey.isCompressed) - return sigSlice, nil -} + // compact = <27+recid(+4 if compressed)><32-byte R><32-byte S> + header := compact[0] + recoveryId := int((header - 27) &^ 4) -func (privateKey *PrivateKey) SignMulti(hash []byte, signerIndex int) ([]byte, error) { - // Parse the private key using the schnorr package - schnorrPrivKey, err := schnorr.ParsePrivKeyBytes(privateKey.PrivateKey.Serialize()) - if err != nil { - return nil, fmt.Errorf("failed to parse Schnorr private key: %v", err) + return &EcdsaSignature{ + R: compact[1 : 1+ecdsaCurveByteLength], + S: compact[1+ecdsaCurveByteLength : 1+2*ecdsaCurveByteLength], + RecoveryId: recoveryId, } +} - // Sign the hash using Schnorr - signature, err := schnorrPrivKey.Sign(hash, make([]byte, 32)) - if err != nil { - return nil, fmt.Errorf("failed to sign hash with Schnorr: %v", err) +// padCurveBytes left-pads b with zero bytes to ecdsaCurveByteLength. RLP +// encodes R/S as minimal big-endian integers, so a value decoded off the +// wire may be shorter than 32 bytes whenever its high-order byte happens to +// be zero; this restores the fixed-width form every other part of the +// signing code expects. +func padCurveBytes(b []byte) []byte { + if len(b) >= ecdsaCurveByteLength { + return b } + padded := make([]byte, ecdsaCurveByteLength) + copy(padded[ecdsaCurveByteLength-len(b):], b) + return padded +} - // Convert [64]byte to []byte - sigArray := signature.Bytes() - sigSlice := make([]byte, 64) - copy(sigSlice, sigArray[:]) - - // Prepend the signer index to the signature - signatureWithIndex := append([]byte{byte(signerIndex)}, sigSlice...) - - return signatureWithIndex, nil +// Bytes returns the 65-byte r‖s‖v encoding of sig, where v is 27+RecoveryId +// (Ethereum's "Electrum" convention). +func (sig *EcdsaSignature) Bytes() []byte { + b := make([]byte, 0, 2*ecdsaCurveByteLength+1) + b = append(b, padCurveBytes(sig.R)...) + b = append(b, padCurveBytes(sig.S)...) + b = append(b, byte(27+sig.RecoveryId)) + return b } -func (privateKey *PrivateKey) SecondSign(hash []byte) ([]byte, error) { - // Parse the private key using the schnorr package - schnorrPrivKey, err := schnorr.ParsePrivKeyBytes(privateKey.PrivateKey.Serialize()) - if err != nil { - return nil, fmt.Errorf("failed to parse Schnorr private key: %v", err) +// EcdsaSignatureFromBytes parses the 65-byte r‖s‖v encoding produced by +// EcdsaSignature.Bytes. +func EcdsaSignatureFromBytes(b []byte) (*EcdsaSignature, error) { + if len(b) != 2*ecdsaCurveByteLength+1 { + return nil, fmt.Errorf("EcdsaSignatureFromBytes: expected %d bytes, got %d", 2*ecdsaCurveByteLength+1, len(b)) } - // Sign the hash using Schnorr - signature, err := schnorrPrivKey.Sign(hash, make([]byte, 32)) - if err != nil { - return nil, fmt.Errorf("failed to create second Schnorr signature: %v", err) + v := b[2*ecdsaCurveByteLength] + if v < 27 || v > 30 { + return nil, fmt.Errorf("EcdsaSignatureFromBytes: invalid v byte %d", v) } - // Convert [64]byte to []byte - sigArray := signature.Bytes() - sigSlice := make([]byte, 64) - copy(sigSlice, sigArray[:]) - - return sigSlice, nil + return &EcdsaSignature{ + R: append([]byte{}, b[:ecdsaCurveByteLength]...), + S: append([]byte{}, b[ecdsaCurveByteLength:2*ecdsaCurveByteLength]...), + RecoveryId: int(v) - 27, + }, nil } diff --git a/crypto/private_key_test.go b/crypto/private_key_test.go index 5bf2cc8..deb82c8 100644 --- a/crypto/private_key_test.go +++ b/crypto/private_key_test.go @@ -26,7 +26,6 @@ func TestPrivateKeyToAddress(t *testing.T) { fixture := GetIdentityFixture() privateKey, _ := PrivateKeyFromPassphrase(fixture.Passphrase) - privateKey.PublicKey.Network.Version = 0x1e assert := assert.New(t) assert.Equal(fixture.Data.Address, privateKey.ToAddress()) diff --git a/crypto/public_key.go b/crypto/public_key.go index 76ee665..b0de172 100644 --- a/crypto/public_key.go +++ b/crypto/public_key.go @@ -12,8 +12,8 @@ import ( "fmt" "strings" - "github.com/btcsuite/btcd/btcec" - "github.com/ellemouton/schnorr" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" "golang.org/x/crypto/sha3" ) @@ -34,12 +34,12 @@ func PublicKeyFromHex(publicKeyHex string) (*PublicKey, error) { } func PublicKeyFromBytes(bytes []byte) (*PublicKey, error) { - publicKey, err := btcec.ParsePubKey(bytes, btcec.S256()) + publicKey, err := secp256k1.ParsePubKey(bytes) if err != nil { return nil, err } isCompressed := false - if len(bytes) == btcec.PubKeyBytesLenCompressed { + if len(bytes) == secp256k1.PubKeyBytesLenCompressed { isCompressed = true } return &PublicKey{ @@ -102,38 +102,43 @@ func (publicKey *PublicKey) SerializeUncompressed() []byte { return publicKey.PublicKey.SerializeUncompressed() } -func (publicKey *PublicKey) Verify(signature []byte, hash []byte) (bool, error) { - return publicKey.VerifySchnorr(signature, hash) -} - -func (publicKey *PublicKey) VerifySchnorr(signature []byte, hash []byte) (bool, error) { - if len(signature) != 64 { - return false, fmt.Errorf("VerifySchnorr: signature is %d bytes, should be 64", len(signature)) +// RecoverPublicKey recovers the public key that produced sig over hash. +// isCompressed must match how the signer's key was represented when signing +// (PrivateKey.Sign uses the signing key's own isCompressed value). +func RecoverPublicKey(hash []byte, sig *EcdsaSignature, isCompressed bool) (*PublicKey, error) { + if len(sig.R) != ecdsaCurveByteLength || len(sig.S) != ecdsaCurveByteLength { + return nil, fmt.Errorf("RecoverPublicKey: R and S must each be %d bytes", ecdsaCurveByteLength) } - if len(hash) != 32 { - return false, fmt.Errorf("VerifySchnorr: message hash is %d bytes, should be 32", len(hash)) + + header := byte(27 + sig.RecoveryId) + if isCompressed { + header += 4 } - // Parse the signature using the schnorr package - sig, err := schnorr.NewSignatureFromBytes(signature) + compact := make([]byte, 0, 1+2*ecdsaCurveByteLength) + compact = append(compact, header) + compact = append(compact, sig.R...) + compact = append(compact, sig.S...) + + pubKey, _, err := ecdsa.RecoverCompact(compact, hash) if err != nil { - return false, fmt.Errorf("VerifySchnorr: failed to parse signature: %v", err) + return nil, fmt.Errorf("RecoverPublicKey: %v", err) } - // Parse the public key using the schnorr package - var schnorrPubKey *schnorr.PublicKey - if len(publicKey.PublicKey.SerializeCompressed()) == 33 { - schnorrPubKey, err = schnorr.ParsePlainPubKey(publicKey.PublicKey.SerializeCompressed()) - } else { - schnorrPubKey, err = schnorr.ParseXOnlyPubKey(publicKey.PublicKey.SerializeCompressed()) - } + return &PublicKey{ + PublicKey: pubKey, + isCompressed: isCompressed, + Network: GetNetwork(), + }, nil +} +// Verify reports whether sig, over hash, was produced by publicKey's private +// key, by recovering the actual signer and comparing it to publicKey. +func (publicKey *PublicKey) Verify(hash []byte, sig *EcdsaSignature) (bool, error) { + recovered, err := RecoverPublicKey(hash, sig, publicKey.isCompressed) if err != nil { - return false, fmt.Errorf("VerifySchnorr: failed to parse public key: %v", err) + return false, err } - // Verify the signature - err = sig.Verify(schnorrPubKey, hash) // Assuming `Verify` returns only bool - - return err == nil, err + return recovered.PublicKey.IsEqual(publicKey.PublicKey), nil } diff --git a/crypto/rlp.go b/crypto/rlp.go index f79a36c..310d5c8 100644 --- a/crypto/rlp.go +++ b/crypto/rlp.go @@ -150,8 +150,10 @@ func (l *RlpList) DecodeRLP(data []byte) (int, error) { } body := data[uint64(prefixLen) : uint64(prefixLen)+dataLen] + preallocated := len(*l) - for i := 0; len(body) > 0; i++ { + i := 0 + for ; len(body) > 0; i++ { var item RlpItem if i < len(*l) { item = (*l)[i] @@ -168,6 +170,13 @@ func (l *RlpList) DecodeRLP(data []byte) (int, error) { body = body[consumed:] } + // If the source list has fewer elements than the pre-typed schema the + // caller supplied, some of those items would otherwise be silently left + // at their zero value (e.g. a *RlpBigInt with a nil X) instead of erroring. + if i < preallocated { + return 0, ErrRlpUnexpectedEndOfData + } + return prefixLen + int(dataLen), nil } diff --git a/crypto/rlp_test.go b/crypto/rlp_test.go index 0a4249a..8f7cb8f 100644 --- a/crypto/rlp_test.go +++ b/crypto/rlp_test.go @@ -203,6 +203,22 @@ func TestRlpDecodeListAppendsExtraItems(t *testing.T) { assert.Equal(RlpBytes{0x03}, *(*decoded)[2].(*RlpBytes)) } +// TestRlpDecodeListShorterThanSchemaErrors confirms a list decoded into MORE +// pre-typed slots than are actually present in the data errors out, rather +// than silently leaving the unfilled slots (e.g. a *RlpBigInt with a nil X) +// at their zero value for the caller to trip over later. +func TestRlpDecodeListShorterThanSchemaErrors(t *testing.T) { + assert := assert.New(t) + + list := NewRlpList(&RlpBytes{0x01}, &RlpBytes{0x02}) + encoded, err := RlpEncode(list) + assert.NoError(err) + + decoded := NewRlpList(&RlpBytes{}, &RlpBytes{}, &RlpBytes{}, &RlpBytes{}) + _, err = RlpDecode(encoded, decoded) + assert.ErrorIs(err, ErrRlpUnexpectedEndOfData) +} + func TestRlpDecodeEmptyDataError(t *testing.T) { assert := assert.New(t) diff --git a/crypto/serdeser_test.go b/crypto/serdeser_test.go deleted file mode 100644 index 94a3da8..0000000 --- a/crypto/serdeser_test.go +++ /dev/null @@ -1,71 +0,0 @@ -// This file is part of Ark Go Crypto. -// -// (c) Ark Ecosystem -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -package crypto - -import ( - "encoding/json" - "log" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -func compareTransactions(t *testing.T, expected, actual Transaction) { - assert := assert.New(t) - assert.Equal(expected.Amount, actual.Amount, "Amount does not match") - assert.Equal(expected.Fee, actual.Fee, "Fee does not match") - assert.Equal(expected.Expiration, actual.Expiration, "Expiration does not match") - assert.Equal(expected.Id, actual.Id, "Id does not match") - assert.Equal(expected.Network, actual.Network, "Network does not match") - assert.Equal(expected.Nonce, actual.Nonce, "Nonce does not match") - assert.Equal(expected.RecipientId, actual.RecipientId, "RecipientId does not match") - assert.Equal(expected.SenderPublicKey, actual.SenderPublicKey, "SenderPublicKey does not match") - assert.Equal(expected.Signature, actual.Signature, "Signature does not match") - assert.Equal(expected.Type, actual.Type, "Type does not match") - assert.Equal(expected.TypeGroup, actual.TypeGroup, "TypeGroup does not match") - assert.Equal(expected.Version, actual.Version, "Version does not match") -} - -func commonSerDeserTest(t *testing.T, fixturePath string) { - fixtureJson := []byte(GetFile(fixturePath)) - - var fixture TestingFixture - - err := json.Unmarshal(fixtureJson, &fixture) - if err != nil { - log.Fatalf("Cannot parse fixture JSON %s: %s", fixturePath, err) - } - - fixture.Transaction.Serialized = HexDecode(fixture.SerializedHex) - - transaction := DeserializeTransaction(fixture.SerializedHex) - - assert := assert.New(t) - - compareTransactions(t, fixture.Transaction, *transaction) - - assert.Equal(fixture.SerializedHex, HexEncode(transaction.Serialize(true, true, true))) - - assert.True(transaction.Verify(&fixture.MultiSignatureAsset)) -} - -func TestSerDeser(t *testing.T) { - directory := "fixtures/transactions/" - files, _ := filepath.Glob(directory + "*/*.json") - - for _, file := range files { - test := func (t *testing.T) { - commonSerDeserTest(t, file) - } - - subTestName := file[len(directory):len(file) - len(".json")] - - t.Run(subTestName, test) - } -} diff --git a/crypto/serializer.go b/crypto/serializer.go index b532266..4ec0bfd 100644 --- a/crypto/serializer.go +++ b/crypto/serializer.go @@ -8,173 +8,65 @@ package crypto import ( - "bytes" - "encoding/binary" - "log" - "strings" + "math/big" ) -func writeNumberAsByte(ser *bytes.Buffer, num interface{}, name string) { - numInt := num.(int) - - if numInt > 0xFF { - log.Fatal("Cannot serialize: max supported", name, "is 256. Provided:", num) - } - - ser.WriteByte(uint8(numInt)) -} - -func (transaction *Transaction) Serialize(includeSignature bool, includeSecondSignature bool, includeMultiSignatures bool) []byte { - ser := new(bytes.Buffer) - - transaction.serializeHeader(ser) - transaction.serializeVendorField(ser) - transaction.serializeTypeSpecific(ser) - transaction.serializeSignatures(ser, includeSignature, includeSecondSignature, includeMultiSignatures) - - return ser.Bytes() -} - -func (transaction *Transaction) serializeHeader(ser *bytes.Buffer) { - ser.WriteByte(uint8(0xFF)) - - ser.WriteByte(transaction.Version) - - if transaction.Network == 0 { - ser.WriteByte(GetNetwork().Version) - } else { - ser.WriteByte(transaction.Network) - } - - binary.Write(ser, binary.LittleEndian, transaction.TypeGroup) - binary.Write(ser, binary.LittleEndian, transaction.Type) - binary.Write(ser, binary.LittleEndian, transaction.Nonce) - if transaction.SenderPublicKey != "" { - ser.Write(HexDecode(transaction.SenderPublicKey)) - } - binary.Write(ser, binary.LittleEndian, uint64(transaction.Fee)) -} - -func (transaction *Transaction) serializeVendorField(ser *bytes.Buffer) { - if transaction.VendorField != "" { - writeNumberAsByte(ser, len(transaction.VendorField), "vendorField") - ser.Write([]byte(transaction.VendorField)) - } else { - ser.WriteByte(uint8(0x00)) - } -} - -func (transaction *Transaction) serializeTypeSpecific(ser *bytes.Buffer) { - switch transaction.Type { - case TRANSACTION_TYPES.Transfer: - transaction.serializeTransfer(ser) - case TRANSACTION_TYPES.ValidatorRegistration: - transaction.serializeValidatorRegistration(ser) - case TRANSACTION_TYPES.Vote: - transaction.serializeVote(ser) - case TRANSACTION_TYPES.MultiSignatureRegistration: - transaction.serializeMultiSignatureRegistration(ser) - case TRANSACTION_TYPES.MultiPayment: - transaction.serializeMultiPayment(ser) - case TRANSACTION_TYPES.ValidatorResignation: - transaction.serializeValidatorResignation(ser) - case TRANSACTION_TYPES.UsernameRegistration: - transaction.serializeUsernameRegistration(ser) - case TRANSACTION_TYPES.UsernameResignation: - transaction.serializeUsernameResignation(ser) - } -} - -func (transaction *Transaction) serializeSignatures(ser *bytes.Buffer, includeSignature bool, includeSecondSignature bool, includeMultiSignatures bool) { - if includeSignature && transaction.Signature != "" { - ser.Write(HexDecode(transaction.Signature)) - } - - if includeSecondSignature && transaction.SecondSignature != "" { - ser.Write(HexDecode(transaction.SecondSignature)) - } - - if includeMultiSignatures && len(transaction.Signatures) > 0 { - ser.Write(HexDecode(strings.Join(transaction.Signatures, ""))) +// Serialize encodes the transaction as an RLP list: +// [nonce, gasPrice, gasLimit, to, value, data, v, r, s]. +// +// If skipSignature is true, or no signature has been set on the transaction +// yet, the v/r/s slots are replaced with the EIP-155 placeholder +// [chainId, 0, 0] — this is the form that gets keccak256-hashed to produce +// the hash a signer signs (see Transaction.SigningHash). Once R and S are +// populated, Serialize(false) embeds the real EIP-155-encoded v +// (v = recoveryId + chainId*2 + 35) alongside r and s — this is the final +// wire encoding of a signed transaction. +func (transaction *Transaction) Serialize(skipSignature bool) ([]byte, error) { + var toBytes []byte + if transaction.To != "" { + var err error + toBytes, err = AddressToBytes(transaction.To) + if err != nil { + return nil, err + } } -} - -func stripAddressPrefix(recipientId string) string { - address := recipientId[2:] - if strings.HasPrefix(address, "0x") { - address = address[2:] + to := RlpBytes(toBytes) + data := RlpBytes(transaction.Data) + + items := []RlpItem{ + NewRlpBigInt(bigIntOrZero(transaction.Nonce)), + NewRlpBigInt(bigIntOrZero(transaction.GasPrice)), + NewRlpBigInt(bigIntOrZero(transaction.GasLimit)), + &to, + NewRlpBigInt(bigIntOrZero(transaction.Value)), + &data, } - return address -} - - -func (transaction *Transaction) serializeTransfer(ser *bytes.Buffer) { - binary.Write(ser, binary.LittleEndian, uint64(transaction.Amount)) - binary.Write(ser, binary.LittleEndian, transaction.Expiration) - - address := stripAddressPrefix(transaction.RecipientId) - - recipientBytes := HexDecode(address) - ser.Write(recipientBytes) -} - -func (transaction *Transaction) serializeValidatorRegistration(ser *bytes.Buffer) { - ser.Write(HexDecode(transaction.Asset.Validator.ValidatorPublicKey)) -} - -func (transaction *Transaction) serializeUsernameRegistration(ser *bytes.Buffer) { - // Write the length of the username - username := transaction.Asset.Username.Username - writeNumberAsByte(ser, len(username), "username length") - - // Write the username - ser.Write([]byte(username)) -} + chainId := big.NewInt(int64(GetNetwork().ChainId)) -func (transaction *Transaction) serializeVote(ser *bytes.Buffer) { - // Serialize Votes - votes := transaction.Asset.Votes - unvotes := transaction.Asset.Unvotes + if !skipSignature && len(transaction.R) > 0 && len(transaction.S) > 0 { + v := new(big.Int).Add(big.NewInt(int64(transaction.V)), new(big.Int).Mul(chainId, big.NewInt(2))) + v.Add(v, big.NewInt(35)) - // Write the number of votes - writeNumberAsByte(ser, len(votes), "number of votes") - - // Write each vote in hexadecimal format - for _, vote := range votes { - ser.Write(HexDecode(vote)) - } - - // Write the number of unvotes - writeNumberAsByte(ser, len(unvotes), "number of unvotes") - - // Write each unvote in hexadecimal format - for _, unvote := range unvotes { - ser.Write(HexDecode(unvote)) + items = append(items, + NewRlpBigInt(v), + NewRlpBigInt(new(big.Int).SetBytes(transaction.R)), + NewRlpBigInt(new(big.Int).SetBytes(transaction.S)), + ) + } else { + items = append(items, + NewRlpBigInt(chainId), + NewRlpBigInt(big.NewInt(0)), + NewRlpBigInt(big.NewInt(0)), + ) } -} -func (transaction *Transaction) serializeMultiSignatureRegistration(ser *bytes.Buffer) { - publicKeys := transaction.Asset.MultiSignature.PublicKeys - - ser.WriteByte(transaction.Asset.MultiSignature.Min) - writeNumberAsByte(ser, len(publicKeys), "number of public keys in multisig") - ser.Write(HexDecode(strings.Join(publicKeys, ""))) + return NewRlpList(items...).EncodeRLP() } -func (transaction *Transaction) serializeMultiPayment(ser *bytes.Buffer) { - binary.Write(ser, binary.LittleEndian, uint16(len(transaction.Asset.Payments))) - - for _, element := range transaction.Asset.Payments { - binary.Write(ser, binary.LittleEndian, uint64(element.Amount)) - ser.Write(HexDecode(stripAddressPrefix(element.RecipientId))) +func bigIntOrZero(x *big.Int) *big.Int { + if x == nil { + return big.NewInt(0) } -} - -func (transaction *Transaction) serializeValidatorResignation(buffer *bytes.Buffer) { - // No specific data to serialize for validator resignation, just parse the signatures -} - -func (transaction *Transaction) serializeUsernameResignation(ser *bytes.Buffer) { - // No specific data to serialize for username resignation, just parse the signatures + return x } diff --git a/crypto/structs.go b/crypto/structs.go index f39aae9..43a6213 100644 --- a/crypto/structs.go +++ b/crypto/structs.go @@ -8,96 +8,50 @@ package crypto import ( - "encoding/json" - "strconv" + "math/big" "time" - "github.com/btcsuite/btcd/btcec" + "github.com/decred/dcrd/dcrec/secp256k1/v4" blst "github.com/supranational/blst/bindings/go" ) -type FlexToshi uint64 - type Network struct { Epoch time.Time - Version byte + ChainId int Wif byte } type PrivateKey struct { - *btcec.PrivateKey + *secp256k1.PrivateKey PublicKey *PublicKey } type PublicKey struct { - *btcec.PublicKey + *secp256k1.PublicKey isCompressed bool Network *Network } -type TransactionTypes struct { - Transfer uint16 - ValidatorRegistration uint16 - Vote uint16 - MultiSignatureRegistration uint16 - MultiPayment uint16 - ValidatorResignation uint16 - UsernameRegistration uint16 - UsernameResignation uint16 -} - -type TransactionTypeGroups struct { - Test uint32 - Core uint32 -} - -type TransactionFees struct { - Transfer FlexToshi - ValidatorRegistration FlexToshi - Vote FlexToshi - MultiSignatureRegistration FlexToshi - MultiPayment FlexToshi - ValidatorResignation FlexToshi - UsernameRegistration FlexToshi - UsernameResignation FlexToshi -} - -func (fi *FlexToshi) UnmarshalJSON(b []byte) error { - if b[0] != '"' { - return json.Unmarshal(b, (*uint64)(fi)) - } - var s string - if err := json.Unmarshal(b, &s); err != nil { - return err - } - i, err := strconv.ParseInt(s, 10, 64) - if err != nil { - return err - } - *fi = FlexToshi(i) - return nil -} - +// Transaction is a Mainsail (EVM-compatible) transaction: an RLP-encoded +// envelope authenticated with a recoverable ECDSA secp256k1 signature. +// Nonce/GasPrice/GasLimit/Value are arbitrary-precision to accommodate +// wei-scale amounts. R and S are each 32 bytes; V is the raw recovery id +// (0-3), not yet EIP-155 encoded — that encoding is applied at serialize +// time using the configured network's chain id. type Transaction struct { - Amount FlexToshi `json:"amount,omitempty"` - Asset *TransactionAsset `json:"asset,omitempty"` - Expiration uint32 `json:"expiration,omitempty"` - Fee FlexToshi `json:"fee,omitempty"` - Id string `json:"id,omitempty"` - Network byte `json:"network,omitempty"` - Nonce uint64 `json:"nonce,omitempty,string"` - RecipientId string `json:"recipientId,omitempty"` - SecondSenderPublicKey string `json:"secondSenderPublicKey,omitempty"` - SecondSignature string `json:"secondSignature,omitempty"` - SenderPublicKey string `json:"senderPublicKey,omitempty"` - Serialized []byte `json:"serialized,omitempty"` - Signature string `json:"signature,omitempty"` - Signatures []string `json:"signatures,omitempty"` - Timestamp int32 `json:"timestamp,omitempty"` - Type uint16 `json:"type"` - TypeGroup uint32 `json:"typeGroup"` - VendorField string `json:"vendorField,omitempty"` - Version byte `json:"version,omitempty"` + Nonce *big.Int `json:"nonce,omitempty"` + GasPrice *big.Int `json:"gasPrice,omitempty"` + GasLimit *big.Int `json:"gasLimit,omitempty"` + To string `json:"to,omitempty"` + Value *big.Int `json:"value,omitempty"` + Data []byte `json:"data,omitempty"` + V int `json:"v"` + R []byte `json:"r,omitempty"` + S []byte `json:"s,omitempty"` + SenderPublicKey string `json:"senderPublicKey,omitempty"` + From string `json:"from,omitempty"` + Hash string `json:"hash,omitempty"` + Serialized []byte `json:"serialized,omitempty"` } type Message struct { @@ -106,37 +60,6 @@ type Message struct { Signature string `json:"signature"` } -//////////////////////////////////////////////////////////////////////////////// -// TRANSACTION ASSETS ////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -type TransactionAsset struct { - Votes []string `json:"votes,omitempty"` - Unvotes []string `json:"unvotes,omitempty"` - Validator *ValidatorAsset `json:"validator,omitempty"` - Username *UsernameAsset `json:"validator,omitempty"` - MultiSignature *MultiSignatureRegistrationAsset `json:"multiSignature,omitempty"` - Payments []*MultiPaymentAsset `json:"payments,omitempty"` -} - -type ValidatorAsset struct { - ValidatorPublicKey string `json:"validatorPublicKey,omitempty"` -} - -type UsernameAsset struct { - Username string `json:"username,omitempty"` -} - -type MultiSignatureRegistrationAsset struct { - Min byte `json:"min,omitempty"` - PublicKeys []string `json:"publicKeys,omitempty"` -} - -type MultiPaymentAsset struct { - Amount FlexToshi `json:"amount,omitempty"` - RecipientId string `json:"recipientId,omitempty"` -} - type BLSPrivateKey struct { PrivateKey *blst.SecretKey } diff --git a/crypto/transaction.go b/crypto/transaction.go index 763c15c..2d905d5 100644 --- a/crypto/transaction.go +++ b/crypto/transaction.go @@ -8,203 +8,124 @@ package crypto import ( - "crypto/sha256" "encoding/json" - "fmt" - "log" + "errors" "github.com/fatih/structs" + "golang.org/x/crypto/sha3" ) -func (transaction *Transaction) GetId() string { - return fmt.Sprintf("%x", sha256.Sum256(transaction.Serialize(true, true, true))) +var ErrTransactionNotSigned = errors.New("crypto: transaction has no senderPublicKey to verify against") + +func keccak256(data []byte) []byte { + hash := sha3.NewLegacyKeccak256() + hash.Write(data) + return hash.Sum(nil) } -func (transaction *Transaction) Sign(passphrase string) { - privateKey, err := PrivateKeyFromPassphrase(passphrase) +// SigningHash returns the keccak256 hash of the transaction's unsigned RLP +// encoding (v/r/s replaced with the EIP-155 [chainId, 0, 0] placeholder) — +// this is the message a signer signs and a verifier/recoverer checks against. +func (transaction *Transaction) SigningHash() ([]byte, error) { + serialized, err := transaction.Serialize(true) if err != nil { - log.Printf("Error deriving private key from passphrase: %v\n", err) - return - } - - transaction.SenderPublicKey = HexEncode(privateKey.PublicKey.Serialize()) - - hash := sha256.Sum256(transaction.Serialize(false, false, false)) - - signature, err := privateKey.Sign(hash[:]) - if err == nil { - transaction.Signature = HexEncode(signature) - } else { - log.Printf("Error signing transaction: %v\n", err) + return nil, err } + return keccak256(serialized), nil } -func (transaction *Transaction) SignMulti(signerIndex int, passphrase string) { - privateKey, err := PrivateKeyFromPassphrase(passphrase) +// GetHash returns the hex-encoded keccak256 hash of the transaction's full +// RLP encoding. Once the transaction is signed this is its final hash/ID; +// before signing it is identical to SigningHash (no v/r/s to embed yet). +func (transaction *Transaction) GetHash() (string, error) { + serialized, err := transaction.Serialize(false) if err != nil { - log.Printf("Error deriving private key from passphrase: %v\n", err) - return - } - - hash := sha256.Sum256(transaction.Serialize(false, false, false)) - - signature, err := privateKey.SignMulti(hash[:], signerIndex) - if err == nil { - transaction.Signatures = append(transaction.Signatures, HexEncode(signature)) - } else { - log.Printf("Error signing multi-signature transaction: %v\n", err) + return "", err } + return HexEncode(keccak256(serialized)), nil } -func (transaction *Transaction) SecondSign(passphrase string) { +// Sign derives a private key from passphrase, signs the transaction, and +// populates SenderPublicKey, From, V, R, S, Hash, and Serialized. +func (transaction *Transaction) Sign(passphrase string) error { privateKey, err := PrivateKeyFromPassphrase(passphrase) if err != nil { - log.Printf("Error deriving private key from passphrase: %v\n", err) - return + return err } - hash := sha256.Sum256(transaction.Serialize(true, false, false)) + transaction.SenderPublicKey = privateKey.PublicKey.ToHex() + transaction.From = privateKey.PublicKey.ToAddress() - signature, err := privateKey.SecondSign(hash[:]) - if err == nil { - transaction.SecondSignature = HexEncode(signature) - } else { - log.Printf("Error creating second signature: %v\n", err) - } -} - -func (transaction *Transaction) VerifyMultiSignature(multiSignatureAsset *MultiSignatureRegistrationAsset) (bool, error) { - hash := sha256.Sum256(transaction.Serialize(false, false, false)) - - publicKeyIndexes := make(map[int]bool) - numVerified := 0 - - for i := 0; i < len(transaction.Signatures); i++ { - if len(transaction.Signatures[i]) < 2 { - return false, fmt.Errorf("VerifyMultiSignature: signature %d too short to contain index", i) - } - publicKeyIndex := int(HexDecode(transaction.Signatures[i][:2])[0]) - signature := HexDecode(transaction.Signatures[i][2:]) - - if publicKeyIndexes[publicKeyIndex] { - return false, fmt.Errorf("VerifyMultiSignature: duplicate signer index: %d", publicKeyIndex) - } - - if publicKeyIndex >= len(multiSignatureAsset.PublicKeys) { - return false, fmt.Errorf( - "VerifyMultiSignature: signer index too large: %d, total of %d "+ - "signers have been registered", - publicKeyIndex, len(multiSignatureAsset.PublicKeys)) - } - - publicKeyIndexes[publicKeyIndex] = true - - publicKey, err := PublicKeyFromBytes(HexDecode(multiSignatureAsset.PublicKeys[publicKeyIndex])) - if err != nil { - return false, err - } - - verified, err := publicKey.Verify(signature, hash[:]) - if err != nil { - return false, fmt.Errorf("VerifyMultiSignature: error verifying signature %d: %v", i, err) - } - - if verified { - numVerified++ - } - - if numVerified >= int(multiSignatureAsset.Min) { - return true, nil - } - - if len(transaction.Signatures)-(i+1-numVerified) < int(multiSignatureAsset.Min) { - return false, fmt.Errorf( - "VerifyMultiSignature: less than the minimum %d signatures verified successfully", - multiSignatureAsset.Min) - } + signingHash, err := transaction.SigningHash() + if err != nil { + return err } - return false, fmt.Errorf( - "VerifyMultiSignature: less than the minimum %d signatures verified successfully (checked all)", - multiSignatureAsset.Min) -} + sig := privateKey.Sign(signingHash) + transaction.V = sig.RecoveryId + transaction.R = sig.R + transaction.S = sig.S -func (transaction *Transaction) Verify(multiSignatureAsset ...*MultiSignatureRegistrationAsset) (bool, error) { - if len(multiSignatureAsset) == 1 && multiSignatureAsset[0].Min > 0 { - return transaction.VerifyMultiSignature(multiSignatureAsset[0]) + hash, err := transaction.GetHash() + if err != nil { + return err } + transaction.Hash = hash - publicKey, err := PublicKeyFromBytes(HexDecode(transaction.SenderPublicKey)) + serialized, err := transaction.Serialize(false) if err != nil { - return false, err + return err } + transaction.Serialized = serialized - hash := sha256.Sum256(transaction.Serialize(false, false, true)) - - return publicKey.Verify(HexDecode(transaction.Signature), hash[:]) -} - -func (transaction *Transaction) SecondVerify(secondPublicKey *PublicKey) (bool, error) { - hash := sha256.Sum256(transaction.Serialize(true, false, false)) - - return secondPublicKey.Verify(HexDecode(transaction.SecondSignature), hash[:]) + return nil } -func (transaction *Transaction) ParseSignatures(sigOffset int) *Transaction { - signatures := transaction.Serialized[sigOffset:] - signaturesLen := len(signatures) - - if signaturesLen == 0 { - transaction.Signature = "" - return transaction +// RecoverSender recovers the sender's public key and address from the +// transaction's signature and populates SenderPublicKey and From. +// +// isCompressed is hardcoded to true: PrivateKeyFromBytes (the only private +// key construction path in this SDK) always produces a compressed key, so +// every signature this SDK itself produces was made with one. +func (transaction *Transaction) RecoverSender() error { + signingHash, err := transaction.SigningHash() + if err != nil { + return err } - return transaction.ParseSignaturesSchnorr(signatures) -} - -func (transaction *Transaction) ParseSignaturesSchnorr(signatures []byte) *Transaction { - const schnorrSignatureLen = 64 - - signaturesLen := len(signatures) - o := 0 + sig := &EcdsaSignature{R: transaction.R, S: transaction.S, RecoveryId: transaction.V} - canReadNonMultiSignature := func() bool { - remaining := signaturesLen - o - return remaining >= schnorrSignatureLen && remaining%65 != 0 + publicKey, err := RecoverPublicKey(signingHash, sig, true) + if err != nil { + return err } - readSchnorrSignature := func() string { - sig := HexEncode(signatures[o : o+schnorrSignatureLen]) - o += schnorrSignatureLen - return sig - } + transaction.SenderPublicKey = publicKey.ToHex() + transaction.From = publicKey.ToAddress() - if canReadNonMultiSignature() { - transaction.Signature = readSchnorrSignature() - } + return nil +} - if canReadNonMultiSignature() { - transaction.SecondSignature = readSchnorrSignature() +// Verify reports whether the transaction's signature was produced by the +// private key matching its SenderPublicKey. +func (transaction *Transaction) Verify() (bool, error) { + if transaction.SenderPublicKey == "" { + return false, ErrTransactionNotSigned } - if signaturesLen-o == 0 { - return transaction + publicKey, err := PublicKeyFromHex(transaction.SenderPublicKey) + if err != nil { + return false, err } - if (signaturesLen-o)%65 != 0 { - log.Fatalf("Cannot parse Schnorr signatures: remaining bytes not multiple of 65: %d", signaturesLen-o) + signingHash, err := transaction.SigningHash() + if err != nil { + return false, err } - count := (signaturesLen - o) / 65 + sig := &EcdsaSignature{R: transaction.R, S: transaction.S, RecoveryId: transaction.V} - for i := 0; i < count; i++ { - signaturePlusPrefix := HexEncode(signatures[o : o+1+schnorrSignatureLen]) - o += 1 + schnorrSignatureLen - - transaction.Signatures = append(transaction.Signatures, signaturePlusPrefix) - } - - return transaction + return publicKey.Verify(signingHash, sig) } func (transaction *Transaction) ToMap() map[string]interface{} { @@ -212,11 +133,9 @@ func (transaction *Transaction) ToMap() map[string]interface{} { } func (transaction *Transaction) ToJson() (string, error) { - jason, err := json.Marshal(transaction) - + data, err := json.Marshal(transaction) if err != nil { return "", err } - - return string(jason), nil + return string(data), nil } diff --git a/go.mod b/go.mod index 33e20e1..fdbfe7a 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,10 @@ module github.com/arkEcosystem/go-crypto go 1.25.0 require ( - github.com/btcsuite/btcd v0.20.1-beta github.com/btcsuite/btcutil v1.0.2 - github.com/ellemouton/schnorr v0.0.0-20230301092540-7b5fdc085456 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 github.com/fatih/structs v1.1.0 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.11.1 github.com/supranational/blst v0.3.16 github.com/tyler-smith/go-bip39 v1.1.0 golang.org/x/crypto v0.54.0 diff --git a/go.sum b/go.sum index 692535f..21cd325 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,4 @@ github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= -github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= @@ -13,8 +12,10 @@ github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46f github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/ellemouton/schnorr v0.0.0-20230301092540-7b5fdc085456 h1:sG+iDqnXLNIpg7qHZVdkktO20eGZ/Bq2mm9bnIoVTdE= -github.com/ellemouton/schnorr v0.0.0-20230301092540-7b5fdc085456/go.mod h1:ExpwljP7GCmgyX+ThpTNadlb7khA8/X5fvBcAChvF+U= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -28,10 +29,8 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk= -github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= @@ -40,8 +39,6 @@ golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -50,8 +47,6 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 865e8a112b97f2f43d64cc47ad7a5a10eaffbb58 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Thu, 23 Jul 2026 21:26:29 +0400 Subject: [PATCH 05/16] feat: implement abi encode/decode --- crypto/abi.go | 11 ----------- crypto/private_key.go | 3 --- crypto/public_key.go | 4 +--- crypto/rlp.go | 13 ++----------- 4 files changed, 3 insertions(+), 28 deletions(-) diff --git a/crypto/abi.go b/crypto/abi.go index 088c887..f6d1342 100644 --- a/crypto/abi.go +++ b/crypto/abi.go @@ -74,7 +74,6 @@ func AbiAddress(address string) (AbiArg, error) { return AbiArg{Encoded: encoded, Dynamic: false}, nil } -// AbiUint256 encodes a static "uint256" argument. func AbiUint256(x *big.Int) (AbiArg, error) { if x == nil || x.Sign() < 0 { return AbiArg{}, ErrAbiNegativeUint @@ -88,17 +87,14 @@ func AbiUint256(x *big.Int) (AbiArg, error) { return AbiArg{Encoded: encoded, Dynamic: false}, nil } -// AbiBytes encodes a dynamic "bytes" argument. func AbiBytes(data []byte) AbiArg { return AbiArg{Encoded: abiEncodeDynamicBytes(data), Dynamic: true} } -// AbiString encodes a dynamic "string" argument. func AbiString(s string) AbiArg { return AbiArg{Encoded: abiEncodeDynamicBytes([]byte(s)), Dynamic: true} } -// AbiAddressArray encodes a dynamic "address[]" argument. func AbiAddressArray(addresses []string) (AbiArg, error) { body := make([]byte, 0, len(addresses)*abiWordLength) @@ -115,7 +111,6 @@ func AbiAddressArray(addresses []string) (AbiArg, error) { return AbiArg{Encoded: encoded, Dynamic: true}, nil } -// AbiUint256Array encodes a dynamic "uint256[]" argument. func AbiUint256Array(values []*big.Int) (AbiArg, error) { body := make([]byte, 0, len(values)*abiWordLength) @@ -221,7 +216,6 @@ func NewAbiDecoder(data []byte, signature string, argCount int) (*AbiDecoder, er return &AbiDecoder{head: head, tail: body}, nil } -// Address decodes the argIndex-th argument as a static "address". func (d *AbiDecoder) Address(argIndex int) (string, error) { word, err := d.headWord(argIndex) if err != nil { @@ -231,7 +225,6 @@ func (d *AbiDecoder) Address(argIndex int) (string, error) { return AddressFromBytes(word[abiWordLength-abiAddressLength:]), nil } -// Uint256 decodes the argIndex-th argument as a static "uint256". func (d *AbiDecoder) Uint256(argIndex int) (*big.Int, error) { word, err := d.headWord(argIndex) if err != nil { @@ -241,7 +234,6 @@ func (d *AbiDecoder) Uint256(argIndex int) (*big.Int, error) { return new(big.Int).SetBytes(word), nil } -// Bytes decodes the argIndex-th argument as a dynamic "bytes". func (d *AbiDecoder) Bytes(argIndex int) ([]byte, error) { tailData, err := d.dynamicTail(argIndex) if err != nil { @@ -262,7 +254,6 @@ func (d *AbiDecoder) Bytes(argIndex int) ([]byte, error) { return append([]byte{}, tailData[:length]...), nil } -// String decodes the argIndex-th argument as a dynamic "string". func (d *AbiDecoder) String(argIndex int) (string, error) { data, err := d.Bytes(argIndex) if err != nil { @@ -271,7 +262,6 @@ func (d *AbiDecoder) String(argIndex int) (string, error) { return string(data), nil } -// AddressArray decodes the argIndex-th argument as a dynamic "address[]". func (d *AbiDecoder) AddressArray(argIndex int) ([]string, error) { tailData, err := d.dynamicTail(argIndex) if err != nil { @@ -292,7 +282,6 @@ func (d *AbiDecoder) AddressArray(argIndex int) ([]string, error) { return addresses, nil } -// Uint256Array decodes the argIndex-th argument as a dynamic "uint256[]". func (d *AbiDecoder) Uint256Array(argIndex int) ([]*big.Int, error) { tailData, err := d.dynamicTail(argIndex) if err != nil { diff --git a/crypto/private_key.go b/crypto/private_key.go index 311dfde..48a43e6 100644 --- a/crypto/private_key.go +++ b/crypto/private_key.go @@ -79,9 +79,6 @@ func (privateKey *PrivateKey) Serialize() []byte { return privateKey.PrivateKey.Serialize() } -// Sign produces a recoverable ECDSA signature over hash using RFC6979 -// deterministic k and BIP0062 low-S normalization (both handled by -// ecdsa.SignCompact, which cannot fail for a valid key and hash). func (privateKey *PrivateKey) Sign(hash []byte) *EcdsaSignature { compact := ecdsa.SignCompact(privateKey.PrivateKey, hash, privateKey.PublicKey.isCompressed) diff --git a/crypto/public_key.go b/crypto/public_key.go index b0de172..11b65a0 100644 --- a/crypto/public_key.go +++ b/crypto/public_key.go @@ -102,7 +102,6 @@ func (publicKey *PublicKey) SerializeUncompressed() []byte { return publicKey.PublicKey.SerializeUncompressed() } -// RecoverPublicKey recovers the public key that produced sig over hash. // isCompressed must match how the signer's key was represented when signing // (PrivateKey.Sign uses the signing key's own isCompressed value). func RecoverPublicKey(hash []byte, sig *EcdsaSignature, isCompressed bool) (*PublicKey, error) { @@ -132,8 +131,7 @@ func RecoverPublicKey(hash []byte, sig *EcdsaSignature, isCompressed bool) (*Pub }, nil } -// Verify reports whether sig, over hash, was produced by publicKey's private -// key, by recovering the actual signer and comparing it to publicKey. +// Verify recovers the actual signer from sig and compares it to publicKey. func (publicKey *PublicKey) Verify(hash []byte, sig *EcdsaSignature) (bool, error) { recovered, err := RecoverPublicKey(hash, sig, publicKey.isCompressed) if err != nil { diff --git a/crypto/rlp.go b/crypto/rlp.go index 310d5c8..64e3d22 100644 --- a/crypto/rlp.go +++ b/crypto/rlp.go @@ -29,14 +29,12 @@ type RlpItem interface { DecodeRLP(data []byte) (int, error) } -// RlpEncode encodes an RlpItem as RLP. func RlpEncode(item RlpItem) ([]byte, error) { return item.EncodeRLP() } -// RlpDecode decodes RLP-encoded data into item and returns the number of -// bytes read. The given data may be longer than the encoded item, in which -// case the remaining data is ignored. +// RlpDecode's data may be longer than the encoded item; the remainder is +// ignored, and the number of bytes actually consumed is returned. func RlpDecode(data []byte, item RlpItem) (int, error) { return item.DecodeRLP(data) } @@ -180,8 +178,6 @@ func (l *RlpList) DecodeRLP(data []byte) (int, error) { return prefixLen + int(dataLen), nil } -// rlpEncodePrefix encodes the RLP type-and-length prefix for offset -// (rlpStringOffset or rlpListOffset) and the given payload length. func rlpEncodePrefix(length int, offset byte) ([]byte, error) { if length <= 55 { return []byte{offset + byte(length)}, nil @@ -199,7 +195,6 @@ func rlpEncodePrefix(length int, offset byte) ([]byte, error) { return prefix, nil } -// rlpEncodeLength returns the minimal big-endian encoding of length. func rlpEncodeLength(length uint64) []byte { var buf [8]byte @@ -213,9 +208,6 @@ func rlpEncodeLength(length uint64) []byte { return append([]byte{}, buf[8-n:]...) } -// rlpDecodePrefix decodes the RLP type-and-length prefix at the start of -// data, returning which offset (rlpStringOffset or rlpListOffset) applies, -// the payload length, and the prefix length in bytes. func rlpDecodePrefix(data []byte) (offset byte, dataLen uint64, prefixLen int, err error) { if len(data) == 0 { return 0, 0, 0, ErrRlpUnexpectedEndOfData @@ -247,7 +239,6 @@ func rlpDecodePrefix(data []byte) (offset byte, dataLen uint64, prefixLen int, e } } -// rlpReadUint reads a big-endian unsigned integer of the given byte length. func rlpReadUint(data []byte, length int) (uint64, error) { if length > 8 { return 0, ErrRlpTooLarge From 888749fd067ac95f8587a9de1f7d53cc65c79325 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Thu, 23 Jul 2026 21:37:37 +0400 Subject: [PATCH 06/16] feat: implement abi encode/decode --- crypto/abi.go | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/crypto/abi.go b/crypto/abi.go index f6d1342..ba45be0 100644 --- a/crypto/abi.go +++ b/crypto/abi.go @@ -21,26 +21,17 @@ var ( ErrAbiInvalidOffset = errors.New("abi: dynamic value offset is invalid") ) -// AbiFunctionSelector returns the first 4 bytes of keccak256(signature), e.g. -// AbiFunctionSelector("vote(address)"). func AbiFunctionSelector(signature string) []byte { hash := sha3.NewLegacyKeccak256() hash.Write([]byte(signature)) return hash.Sum(nil)[:abiSelectorLength] } -// AbiArg is a single already-ABI-encoded function argument, tagged with -// whether it belongs in the head (static) or tail (dynamic) section of a -// function call's calldata. type AbiArg struct { Encoded []byte Dynamic bool } -// AbiEncodeFunctionCall assembles a full function call: the 4-byte selector -// followed by the head/tail encoding of args, per the Solidity ABI spec — -// static args are encoded directly in the head, dynamic args contribute a -// 32-byte offset in the head and their data in the tail. func AbiEncodeFunctionCall(signature string, args ...AbiArg) []byte { head := make([]byte, 0, len(args)*abiWordLength) tail := []byte{} @@ -64,8 +55,7 @@ func AbiEncodeFunctionCall(signature string, args ...AbiArg) []byte { return result } -// AbiAddress encodes a static "address" argument. address must be a -// "0x"-prefixed, 40-hex-char string. +// address must be a "0x"-prefixed, 40-hex-char string. func AbiAddress(address string) (AbiArg, error) { encoded, err := abiEncodeAddress(address) if err != nil { @@ -131,16 +121,13 @@ func AbiUint256Array(values []*big.Int) (AbiArg, error) { return AbiArg{Encoded: encoded, Dynamic: true}, nil } -// abiPadWordLeft left-pads b into a 32-byte word. Callers must ensure -// len(b) <= abiWordLength. +// Callers must ensure len(b) <= abiWordLength. func abiPadWordLeft(b []byte) []byte { word := make([]byte, abiWordLength) copy(word[abiWordLength-len(b):], b) return word } -// abiEncodeUintWord encodes an arbitrary, possibly caller-supplied uint256, -// rejecting values that don't fit in one 32-byte word. func abiEncodeUintWord(x *big.Int) ([]byte, error) { b := x.Bytes() if len(b) > abiWordLength { @@ -183,18 +170,11 @@ func abiEncodeDynamicBytes(data []byte) []byte { return append(lengthWord, body...) } -// AbiDecoder decodes the calldata of a single, known function call: the -// 4-byte selector followed by a flat sequence of 32-byte head words, one per -// top-level argument, where dynamic arguments' head word is an offset -// pointing into the tail section. type AbiDecoder struct { head [][]byte tail []byte } -// NewAbiDecoder validates that data starts with the given function selector -// and splits the remaining calldata into its head words and tail region, -// where argCount is the function's total number of top-level arguments. func NewAbiDecoder(data []byte, signature string, argCount int) (*AbiDecoder, error) { if len(data) < abiSelectorLength { return nil, ErrAbiUnexpectedEndOfData @@ -323,8 +303,6 @@ func (d *AbiDecoder) headWord(argIndex int) ([]byte, error) { return d.head[argIndex], nil } -// dynamicTail follows the offset stored in the argIndex-th head word and -// returns the tail data starting at that offset. func (d *AbiDecoder) dynamicTail(argIndex int) ([]byte, error) { word, err := d.headWord(argIndex) if err != nil { From 22c4331b3b8f8a43834ff4122c4dbd762a1bf307 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Thu, 23 Jul 2026 21:42:11 +0400 Subject: [PATCH 07/16] feat: implement abi encode/decode --- crypto/address.go | 4 ---- crypto/contracts.go | 3 --- crypto/message.go | 3 --- crypto/rlp.go | 8 +++----- crypto/transaction.go | 10 ++++------ 5 files changed, 7 insertions(+), 21 deletions(-) diff --git a/crypto/address.go b/crypto/address.go index 92b7c68..5528799 100644 --- a/crypto/address.go +++ b/crypto/address.go @@ -25,8 +25,6 @@ func AddressFromPassphrase(passphrase string) (string, error) { return privateKey.ToAddress(), nil } -// AddressToBytes decodes a "0x"-prefixed, 40-hex-char address into its raw -// 20 bytes. func AddressToBytes(address string) ([]byte, error) { if !strings.HasPrefix(address, "0x") || len(address) != 2+AddressByteLength*2 { return nil, ErrInvalidAddress @@ -40,8 +38,6 @@ func AddressToBytes(address string) ([]byte, error) { return addressBytes, nil } -// AddressFromBytes formats raw 20 address bytes as a "0x"-prefixed, -// EIP-55-checksummed address. func AddressFromBytes(addressBytes []byte) string { return "0x" + EIP55Checksum(hex.EncodeToString(addressBytes)) } diff --git a/crypto/contracts.go b/crypto/contracts.go index 825b1ed..eca93af 100644 --- a/crypto/contracts.go +++ b/crypto/contracts.go @@ -1,6 +1,5 @@ package crypto -// Well-known Mainsail system contract addresses. const ( ContractConsensus = "0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1" ContractMultipayment = "0x00EFd0D4639191C49908A7BddbB9A11A994A8527" @@ -8,8 +7,6 @@ const ( ContractBatchTransfer = "0x5a223F4434D5Bd8478100EEb3b0166a57A26350d" ) -// ABI function signatures for the Mainsail system contracts, plus the -// generic ERC-20 functions used by the token convenience builders. const ( AbiSignatureVote = "vote(address)" AbiSignatureUnvote = "unvote()" diff --git a/crypto/message.go b/crypto/message.go index 56878d0..2a8bf38 100644 --- a/crypto/message.go +++ b/crypto/message.go @@ -14,9 +14,6 @@ import ( "github.com/fatih/structs" ) -// SignMessage signs an arbitrary message with the private key derived from -// passphrase. -// // NOTE: this still hashes the message with plain sha256, carried over // unchanged from before the Mainsail migration. Ethereum's personal_sign // convention (keccak256("\x19Ethereum Signed Message:\n" + len(message) + diff --git a/crypto/rlp.go b/crypto/rlp.go index 64e3d22..61acebb 100644 --- a/crypto/rlp.go +++ b/crypto/rlp.go @@ -39,7 +39,6 @@ func RlpDecode(data []byte, item RlpItem) (int, error) { return item.DecodeRLP(data) } -// RlpBytes is an RLP byte string. type RlpBytes []byte func (s *RlpBytes) EncodeRLP() ([]byte, error) { @@ -72,7 +71,6 @@ func (s *RlpBytes) DecodeRLP(data []byte) (int, error) { return prefixLen + int(dataLen), nil } -// RlpBigInt is an RLP-encoded arbitrary-precision, non-negative integer. // Zero always encodes as an empty RLP string. type RlpBigInt struct{ X *big.Int } @@ -106,9 +104,9 @@ func (b *RlpBigInt) DecodeRLP(data []byte) (int, error) { return n, nil } -// RlpList is an ordered RLP list of items. When decoding, positions already -// populated with an item are decoded into that item's concrete type; any -// items beyond the pre-populated length are appended as raw RlpBytes. +// When decoding, positions already populated with an item are decoded into +// that item's concrete type; any items beyond the pre-populated length are +// appended as raw RlpBytes. type RlpList []RlpItem func NewRlpList(items ...RlpItem) *RlpList { diff --git a/crypto/transaction.go b/crypto/transaction.go index 2d905d5..ad3396f 100644 --- a/crypto/transaction.go +++ b/crypto/transaction.go @@ -23,9 +23,8 @@ func keccak256(data []byte) []byte { return hash.Sum(nil) } -// SigningHash returns the keccak256 hash of the transaction's unsigned RLP -// encoding (v/r/s replaced with the EIP-155 [chainId, 0, 0] placeholder) — -// this is the message a signer signs and a verifier/recoverer checks against. +// SigningHash is the message a signer signs and a verifier/recoverer checks +// against. func (transaction *Transaction) SigningHash() ([]byte, error) { serialized, err := transaction.Serialize(true) if err != nil { @@ -34,9 +33,8 @@ func (transaction *Transaction) SigningHash() ([]byte, error) { return keccak256(serialized), nil } -// GetHash returns the hex-encoded keccak256 hash of the transaction's full -// RLP encoding. Once the transaction is signed this is its final hash/ID; -// before signing it is identical to SigningHash (no v/r/s to embed yet). +// Before signing, GetHash is identical to SigningHash (no v/r/s to embed +// yet); once signed, it becomes the transaction's final hash/ID. func (transaction *Transaction) GetHash() (string, error) { serialized, err := transaction.Serialize(false) if err != nil { From 0342f684e8f5d0d06561d3b60e78621bcdc19cbb Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Thu, 23 Jul 2026 21:56:27 +0400 Subject: [PATCH 08/16] feat: implement abi encode/decode --- crypto/message.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crypto/message.go b/crypto/message.go index 2a8bf38..db53546 100644 --- a/crypto/message.go +++ b/crypto/message.go @@ -14,10 +14,6 @@ import ( "github.com/fatih/structs" ) -// NOTE: this still hashes the message with plain sha256, carried over -// unchanged from before the Mainsail migration. Ethereum's personal_sign -// convention (keccak256("\x19Ethereum Signed Message:\n" + len(message) + -// message)) is a separate, not-yet-made design change. func SignMessage(message string, passphrase string) (*Message, error) { privateKey, err := PrivateKeyFromPassphrase(passphrase) if err != nil { From 953a5b571664bd3f3e70ba37c26b65458c10280c Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Thu, 23 Jul 2026 21:58:50 +0400 Subject: [PATCH 09/16] feat: add tx builders --- crypto/builder.go | 261 +++++++++++++++++++++++++- crypto/builder_test.go | 304 +++++++++++++++++++++++++++++++ crypto/deserializer.go | 17 +- crypto/structs.go | 13 ++ crypto/transaction_test.go | 114 ++++++++++++ crypto/transaction_types.go | 211 +++++++++++++++++++++ crypto/transaction_types_test.go | 242 ++++++++++++++++++++++++ 7 files changed, 1148 insertions(+), 14 deletions(-) create mode 100644 crypto/builder_test.go create mode 100644 crypto/transaction_test.go create mode 100644 crypto/transaction_types.go create mode 100644 crypto/transaction_types_test.go diff --git a/crypto/builder.go b/crypto/builder.go index 2eb82be..977e1dd 100644 --- a/crypto/builder.go +++ b/crypto/builder.go @@ -10,7 +10,9 @@ package crypto import ( "encoding/hex" "errors" + "fmt" "math/big" + "regexp" blst "github.com/supranational/blst/bindings/go" ) @@ -22,9 +24,8 @@ var ( DefaultGasLimit = big.NewInt(1_000_000) ) -// NewTransaction returns a Transaction with the default nonce/gasPrice/ -// gasLimit/value builder defaults set. Concrete transaction-type builders -// (BuildTransfer, BuildVote, etc.) are layered on top of this. +// Concrete transaction-type builders (BuildTransfer, BuildVote, etc.) are +// layered on top of this. func NewTransaction() *Transaction { return &Transaction{ Nonce: big.NewInt(1), @@ -34,6 +35,260 @@ func NewTransaction() *Transaction { } } +func BuildTransfer(to string, value *big.Int) (*Transaction, error) { + if _, err := AddressToBytes(to); err != nil { + return nil, err + } + + transaction := NewTransaction() + transaction.To = to + transaction.Value = bigIntOrZero(value) + + return transaction, nil +} + +func BuildVote(validatorAddress string) (*Transaction, error) { + voteArg, err := AbiAddress(validatorAddress) + if err != nil { + return nil, err + } + + transaction := NewTransaction() + transaction.To = ContractConsensus + transaction.Data = AbiEncodeFunctionCall(AbiSignatureVote, voteArg) + transaction.Vote = validatorAddress + + return transaction, nil +} + +func BuildUnvote() *Transaction { + transaction := NewTransaction() + transaction.To = ContractConsensus + transaction.Data = AbiEncodeFunctionCall(AbiSignatureUnvote) + + return transaction +} + +// NOTE: BLS Proof-of-Possession is not yet implemented — the proof argument +// is encoded as empty bytes. This is a known, documented gap: the resulting +// transaction carries a validator public key but no proof, and will likely +// not validate on an actual Mainsail chain until PoP support is added. +func BuildValidatorRegistration(validatorPublicKey string, stake *big.Int) (*Transaction, error) { + if err := validateBLSPublicKey(validatorPublicKey); err != nil { + return nil, err + } + + pubKeyBytes, err := hex.DecodeString(validatorPublicKey) + if err != nil { + return nil, err + } + + transaction := NewTransaction() + transaction.To = ContractConsensus + transaction.Value = bigIntOrZero(stake) + transaction.Data = AbiEncodeFunctionCall(AbiSignatureRegisterValidator, AbiBytes(pubKeyBytes), AbiBytes([]byte{})) + transaction.ValidatorPublicKey = validatorPublicKey + + return transaction, nil +} + +// NOTE: as with BuildValidatorRegistration, BLS Proof-of-Possession is not +// yet implemented; the proof argument is encoded as empty bytes. +func BuildValidatorUpdate(validatorPublicKey string) (*Transaction, error) { + if err := validateBLSPublicKey(validatorPublicKey); err != nil { + return nil, err + } + + pubKeyBytes, err := hex.DecodeString(validatorPublicKey) + if err != nil { + return nil, err + } + + transaction := NewTransaction() + transaction.To = ContractConsensus + transaction.Data = AbiEncodeFunctionCall(AbiSignatureUpdateValidator, AbiBytes(pubKeyBytes), AbiBytes([]byte{})) + transaction.ValidatorPublicKey = validatorPublicKey + + return transaction, nil +} + +func BuildValidatorResignation() *Transaction { + transaction := NewTransaction() + transaction.To = ContractConsensus + transaction.Data = AbiEncodeFunctionCall(AbiSignatureResignValidator) + + return transaction +} + +var ( + ErrInvalidUsername = errors.New("crypto: invalid username") + + usernameCharsetRegexp = regexp.MustCompile(`[^a-z0-9_]`) + usernameEdgeUnderscoreRegexp = regexp.MustCompile(`^_|_$`) + usernameDoubleUnderscoreRegexp = regexp.MustCompile(`__`) +) + +// validateUsername mirrors php-crypto's Helpers::isValidUsername exactly: +// 1-20 characters, lowercase letters/digits/underscore only, no leading or +// trailing underscore, no consecutive underscores. +func validateUsername(username string) error { + if len(username) < 1 || len(username) > 20 { + return fmt.Errorf("%w: must be between 1 and 20 characters long, got %d", ErrInvalidUsername, len(username)) + } + if usernameCharsetRegexp.MatchString(username) { + return fmt.Errorf("%w: can only contain lowercase letters, numbers and underscores", ErrInvalidUsername) + } + if usernameEdgeUnderscoreRegexp.MatchString(username) { + return fmt.Errorf("%w: cannot start or end with an underscore", ErrInvalidUsername) + } + if usernameDoubleUnderscoreRegexp.MatchString(username) { + return fmt.Errorf("%w: cannot contain consecutive underscores", ErrInvalidUsername) + } + return nil +} + +func BuildUsernameRegistration(username string) (*Transaction, error) { + if err := validateUsername(username); err != nil { + return nil, err + } + + transaction := NewTransaction() + transaction.To = ContractUsernames + transaction.Data = AbiEncodeFunctionCall(AbiSignatureRegisterUsername, AbiString(username)) + transaction.Username = username + + return transaction, nil +} + +func BuildUsernameResignation() *Transaction { + transaction := NewTransaction() + transaction.To = ContractUsernames + transaction.Data = AbiEncodeFunctionCall(AbiSignatureResignUsername) + + return transaction +} + +// addresses/amounts must be the same length, with at least one recipient. +// The transaction's native Value is set to the sum of amounts, matching +// php-crypto/typescript-crypto (the multipayment contract call is payable +// and expects the attached value to cover the total being dispersed). +func BuildMultiPayment(addresses []string, amounts []*big.Int) (*Transaction, error) { + if len(addresses) != len(amounts) { + return nil, fmt.Errorf("crypto: multi-payment addresses and amounts must be the same length, got %d and %d", len(addresses), len(amounts)) + } + if len(addresses) == 0 { + return nil, errors.New("crypto: multi-payment requires at least one recipient") + } + + addressesArg, err := AbiAddressArray(addresses) + if err != nil { + return nil, err + } + amountsArg, err := AbiUint256Array(amounts) + if err != nil { + return nil, err + } + + total := big.NewInt(0) + for _, amount := range amounts { + total.Add(total, amount) + } + + transaction := NewTransaction() + transaction.To = ContractMultipayment + transaction.Value = total + transaction.Data = AbiEncodeFunctionCall(AbiSignatureMultipayment, addressesArg, amountsArg) + transaction.PaymentAddresses = addresses + transaction.PaymentAmounts = amounts + + return transaction, nil +} + +// Used directly for calls with no dedicated builder, and as the underlying +// mechanism for BuildBatchTransfer/BuildTokenApprove/BuildTokenTransfer below. +func BuildEvmCall(to string, data []byte) (*Transaction, error) { + if _, err := AddressToBytes(to); err != nil { + return nil, err + } + + transaction := NewTransaction() + transaction.To = to + transaction.Data = data + + return transaction, nil +} + +// recipients/amounts must be the same length, with at least one recipient. +func BuildBatchTransfer(tokenAddress string, recipients []string, amounts []*big.Int) (*Transaction, error) { + if len(recipients) != len(amounts) { + return nil, fmt.Errorf("crypto: batch transfer recipients and amounts must be the same length, got %d and %d", len(recipients), len(amounts)) + } + if len(recipients) == 0 { + return nil, errors.New("crypto: batch transfer requires at least one recipient") + } + + tokenArg, err := AbiAddress(tokenAddress) + if err != nil { + return nil, err + } + recipientsArg, err := AbiAddressArray(recipients) + if err != nil { + return nil, err + } + amountsArg, err := AbiUint256Array(amounts) + if err != nil { + return nil, err + } + + transaction := NewTransaction() + transaction.To = ContractBatchTransfer + transaction.Data = AbiEncodeFunctionCall(AbiSignatureERC20BatchTransferFrom, tokenArg, recipientsArg, amountsArg) + + return transaction, nil +} + +func BuildTokenApprove(tokenAddress string, spender string, amount *big.Int) (*Transaction, error) { + if _, err := AddressToBytes(tokenAddress); err != nil { + return nil, err + } + + spenderArg, err := AbiAddress(spender) + if err != nil { + return nil, err + } + amountArg, err := AbiUint256(amount) + if err != nil { + return nil, err + } + + transaction := NewTransaction() + transaction.To = tokenAddress + transaction.Data = AbiEncodeFunctionCall(AbiSignatureERC20Approve, spenderArg, amountArg) + + return transaction, nil +} + +func BuildTokenTransfer(tokenAddress string, recipient string, amount *big.Int) (*Transaction, error) { + if _, err := AddressToBytes(tokenAddress); err != nil { + return nil, err + } + + recipientArg, err := AbiAddress(recipient) + if err != nil { + return nil, err + } + amountArg, err := AbiUint256(amount) + if err != nil { + return nil, err + } + + transaction := NewTransaction() + transaction.To = tokenAddress + transaction.Data = AbiEncodeFunctionCall(AbiSignatureERC20Transfer, recipientArg, amountArg) + + return transaction, nil +} + func validateBLSPublicKey(publicKey string) error { if len(publicKey) != 96 { return errors.New("invalid BLS public key length") diff --git a/crypto/builder_test.go b/crypto/builder_test.go new file mode 100644 index 0000000..2901e30 --- /dev/null +++ b/crypto/builder_test.go @@ -0,0 +1,304 @@ +// This file is part of Ark Go Crypto. +// +// (c) Ark Ecosystem +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +package crypto + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testPassphrase = "this is a top secret passphrase" + +// signSerializeDeserialize signs transaction with testPassphrase, round-trips +// it through Serialize/DeserializeTransaction (which also runs +// DecodeTransactionArgs), and returns the deserialized result for assertion. +func signSerializeDeserialize(t *testing.T, transaction *Transaction) *Transaction { + t.Helper() + require := require.New(t) + + require.NoError(transaction.Sign(testPassphrase)) + + verified, err := transaction.Verify() + require.NoError(err) + require.True(verified) + + deserialized, err := DeserializeTransaction(HexEncode(transaction.Serialized)) + require.NoError(err) + + deserializedVerified, err := deserialized.Verify() + require.NoError(err) + require.True(deserializedVerified) + + return deserialized +} + +func TestBuildTransferRoundTrip(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + recipient := testAddress(0x01) + transaction, err := BuildTransfer(recipient, big.NewInt(1_000_000)) + require.NoError(err) + + deserialized := signSerializeDeserialize(t, transaction) + + assert.True(IsTransfer(deserialized.Data)) + assert.Equal(recipient, deserialized.To) + assert.Equal(0, big.NewInt(1_000_000).Cmp(deserialized.Value)) +} + +func TestBuildTransferInvalidRecipientErrors(t *testing.T) { + assert := assert.New(t) + + _, err := BuildTransfer("not-an-address", big.NewInt(1)) + assert.ErrorIs(err, ErrInvalidAddress) +} + +func TestBuildVoteRoundTrip(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + validator := testAddress(0x02) + transaction, err := BuildVote(validator) + require.NoError(err) + + deserialized := signSerializeDeserialize(t, transaction) + + assert.True(IsVote(deserialized.Data)) + assert.Equal(ContractConsensus, deserialized.To) + assert.Equal(validator, deserialized.Vote) +} + +func TestBuildUnvoteRoundTrip(t *testing.T) { + assert := assert.New(t) + + transaction := BuildUnvote() + deserialized := signSerializeDeserialize(t, transaction) + + assert.True(IsUnvote(deserialized.Data)) + assert.Equal(ContractConsensus, deserialized.To) +} + +func TestBuildValidatorRegistrationRoundTrip(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + blsPublicKey := "a08058db53e2665c84a40f5152e76dd2b652125a6079130d4c315e728bcf4dd1dfb44ac26e82302331d61977d3141118" + + transaction, err := BuildValidatorRegistration(blsPublicKey, big.NewInt(2_500_000_000)) + require.NoError(err) + + deserialized := signSerializeDeserialize(t, transaction) + + assert.True(IsValidatorRegistration(deserialized.Data)) + assert.Equal(ContractConsensus, deserialized.To) + assert.Equal(blsPublicKey, deserialized.ValidatorPublicKey) + assert.Equal(0, big.NewInt(2_500_000_000).Cmp(deserialized.Value)) +} + +func TestBuildValidatorRegistrationInvalidKeyErrors(t *testing.T) { + assert := assert.New(t) + + _, err := BuildValidatorRegistration("too-short", nil) + assert.Error(err) +} + +func TestBuildValidatorUpdateRoundTrip(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + blsPublicKey := "a08058db53e2665c84a40f5152e76dd2b652125a6079130d4c315e728bcf4dd1dfb44ac26e82302331d61977d3141118" + + transaction, err := BuildValidatorUpdate(blsPublicKey) + require.NoError(err) + + deserialized := signSerializeDeserialize(t, transaction) + + assert.True(IsUpdateValidator(deserialized.Data)) + assert.Equal(blsPublicKey, deserialized.ValidatorPublicKey) +} + +func TestBuildValidatorResignationRoundTrip(t *testing.T) { + assert := assert.New(t) + + transaction := BuildValidatorResignation() + deserialized := signSerializeDeserialize(t, transaction) + + assert.True(IsValidatorResignation(deserialized.Data)) + assert.Equal(ContractConsensus, deserialized.To) +} + +func TestBuildUsernameRegistrationRoundTrip(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + transaction, err := BuildUsernameRegistration("simple_tx_tester") + require.NoError(err) + + deserialized := signSerializeDeserialize(t, transaction) + + assert.True(IsUsernameRegistration(deserialized.Data)) + assert.Equal(ContractUsernames, deserialized.To) + assert.Equal("simple_tx_tester", deserialized.Username) +} + +func TestBuildUsernameRegistrationValidation(t *testing.T) { + assert := assert.New(t) + + cases := []string{ + "", // too short + "this_username_is_way_too_long", // too long + "Invalid", // uppercase + "_leading", // leading underscore + "trailing_", // trailing underscore + "double__underscore", // consecutive underscores + } + + for _, username := range cases { + _, err := BuildUsernameRegistration(username) + assert.ErrorIs(err, ErrInvalidUsername, "username %q should be rejected", username) + } +} + +func TestBuildUsernameResignationRoundTrip(t *testing.T) { + assert := assert.New(t) + + transaction := BuildUsernameResignation() + deserialized := signSerializeDeserialize(t, transaction) + + assert.True(IsUsernameResignation(deserialized.Data)) + assert.Equal(ContractUsernames, deserialized.To) +} + +func TestBuildMultiPaymentRoundTrip(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + addresses := []string{testAddress(0x01), testAddress(0x02)} + amounts := []*big.Int{big.NewInt(111222), big.NewInt(222333)} + + transaction, err := BuildMultiPayment(addresses, amounts) + require.NoError(err) + + assert.Equal(0, big.NewInt(333555).Cmp(transaction.Value)) // sum of amounts + + deserialized := signSerializeDeserialize(t, transaction) + + assert.True(IsMultiPayment(deserialized.Data)) + assert.Equal(ContractMultipayment, deserialized.To) + assert.Equal(addresses, deserialized.PaymentAddresses) + require.Equal(len(amounts), len(deserialized.PaymentAmounts)) + for i, amount := range amounts { + assert.Equal(0, amount.Cmp(deserialized.PaymentAmounts[i])) + } +} + +func TestBuildMultiPaymentMismatchedLengthsErrors(t *testing.T) { + assert := assert.New(t) + + _, err := BuildMultiPayment([]string{testAddress(0x01)}, []*big.Int{}) + assert.Error(err) +} + +func TestBuildMultiPaymentEmptyErrors(t *testing.T) { + assert := assert.New(t) + + _, err := BuildMultiPayment(nil, nil) + assert.Error(err) +} + +func TestBuildEvmCallRoundTrip(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + to := testAddress(0x03) + data := []byte{0xde, 0xad, 0xbe, 0xef} + + transaction, err := BuildEvmCall(to, data) + require.NoError(err) + + deserialized := signSerializeDeserialize(t, transaction) + + // Arbitrary/unrecognized calldata: none of the known predicates match. + assert.False(IsVote(deserialized.Data)) + assert.False(IsUnvote(deserialized.Data)) + assert.False(IsValidatorRegistration(deserialized.Data)) + assert.False(IsUsernameRegistration(deserialized.Data)) + assert.False(IsMultiPayment(deserialized.Data)) + assert.Equal(to, deserialized.To) + assert.Equal(data, deserialized.Data) +} + +func TestBuildBatchTransferRoundTrip(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + token := testAddress(0x04) + recipients := []string{testAddress(0x01), testAddress(0x02)} + amounts := []*big.Int{big.NewInt(100), big.NewInt(200)} + + transaction, err := BuildBatchTransfer(token, recipients, amounts) + require.NoError(err) + + deserialized := signSerializeDeserialize(t, transaction) + + assert.True(IsBatchTransfer(deserialized.Data)) + assert.Equal(ContractBatchTransfer, deserialized.To) +} + +func TestBuildTokenApproveRoundTrip(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + token := testAddress(0x05) + spender := testAddress(0x06) + + transaction, err := BuildTokenApprove(token, spender, big.NewInt(500)) + require.NoError(err) + + deserialized := signSerializeDeserialize(t, transaction) + + assert.True(IsApprove(deserialized.Data)) + assert.False(IsRevoke(deserialized.Data)) + assert.Equal(token, deserialized.To) +} + +func TestBuildTokenApproveZeroAmountIsRevoke(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + transaction, err := BuildTokenApprove(testAddress(0x05), testAddress(0x06), big.NewInt(0)) + require.NoError(err) + + deserialized := signSerializeDeserialize(t, transaction) + + assert.True(IsRevoke(deserialized.Data)) + assert.False(IsApprove(deserialized.Data)) +} + +func TestBuildTokenTransferRoundTrip(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + token := testAddress(0x07) + recipient := testAddress(0x08) + + transaction, err := BuildTokenTransfer(token, recipient, big.NewInt(750)) + require.NoError(err) + + deserialized := signSerializeDeserialize(t, transaction) + + assert.True(IsTokenTransfer(deserialized.Data)) + assert.Equal(token, deserialized.To) +} + +// Dispatch/type-identifier tests (DecodeTransactionArgs, IsVote, IsUnvote, +// ...) live in transaction_types_test.go, alongside the code they cover. diff --git a/crypto/deserializer.go b/crypto/deserializer.go index aeff9d9..01e79af 100644 --- a/crypto/deserializer.go +++ b/crypto/deserializer.go @@ -1,10 +1,3 @@ -// This file is part of Ark Go Crypto. -// -// (c) Ark Ecosystem -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - package crypto import ( @@ -18,10 +11,8 @@ var ( ErrDeserializeInvalidV = errors.New("deserialize: v field does not decode to a valid recovery id") ) -// DeserializeTransaction decodes a hex-encoded RLP transaction envelope -// ([nonce, gasPrice, gasLimit, to, value, data, v, r, s]), reverses the -// EIP-155 v encoding back to a raw recovery id, computes the transaction -// hash, and recovers the sender's public key and address from the signature. +// DeserializeTransaction decodes a hex-encoded RLP transaction envelope: +// [nonce, gasPrice, gasLimit, to, value, data, v, r, s]. func DeserializeTransaction(serializedHex string) (*Transaction, error) { serialized := HexDecode(serializedHex) @@ -77,6 +68,10 @@ func DeserializeTransaction(serializedHex string) (*Transaction, error) { return nil, err } + if err := DecodeTransactionArgs(transaction); err != nil { + return nil, err + } + return transaction, nil } diff --git a/crypto/structs.go b/crypto/structs.go index 43a6213..ea0f4a4 100644 --- a/crypto/structs.go +++ b/crypto/structs.go @@ -52,6 +52,19 @@ type Transaction struct { From string `json:"from,omitempty"` Hash string `json:"hash,omitempty"` Serialized []byte `json:"serialized,omitempty"` + + // The fields below are populated only for the transaction kind they + // apply to, by DecodeTransactionArgs during deserialization; all others + // are left at their zero value. Mainsail transactions carry no explicit + // type field on the wire — use the IsVote/IsUnvote/... family of + // functions (matching typescript-crypto's TransactionTypeIdentifier) to + // check what kind of transaction Data represents. + Vote string `json:"vote,omitempty"` + ValidatorPublicKey string `json:"validatorPublicKey,omitempty"` + ValidatorProof string `json:"validatorProof,omitempty"` + Username string `json:"username,omitempty"` + PaymentAddresses []string `json:"paymentAddresses,omitempty"` + PaymentAmounts []*big.Int `json:"paymentAmounts,omitempty"` } type Message struct { diff --git a/crypto/transaction_test.go b/crypto/transaction_test.go new file mode 100644 index 0000000..89b12b5 --- /dev/null +++ b/crypto/transaction_test.go @@ -0,0 +1,114 @@ +package crypto + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestTransactionSignSerializeDeserializeVerifyRoundTrip proves the full +// Mainsail envelope pipeline end to end: build a generic (EvmCall-shaped) +// transaction, sign it, serialize it, deserialize the wire bytes back, +// confirm every field survived the round trip, and confirm the recovered +// sender and the signature both verify correctly. +func TestTransactionSignSerializeDeserializeVerifyRoundTrip(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + passphrase := "this is a top secret passphrase" + + transaction := NewTransaction() + transaction.Nonce = big.NewInt(7) + transaction.To = ContractConsensus + transaction.Value = big.NewInt(0) + transaction.Data = AbiEncodeFunctionCall(AbiSignatureUnvote) + + require.NoError(transaction.Sign(passphrase)) + + expectedAddress, err := AddressFromPassphrase(passphrase) + require.NoError(err) + assert.Equal(expectedAddress, transaction.From) + + verified, err := transaction.Verify() + require.NoError(err) + assert.True(verified) + + serializedHex := HexEncode(transaction.Serialized) + + deserialized, err := DeserializeTransaction(serializedHex) + require.NoError(err) + + assert.Equal(0, transaction.Nonce.Cmp(deserialized.Nonce)) + assert.Equal(0, transaction.GasPrice.Cmp(deserialized.GasPrice)) + assert.Equal(0, transaction.GasLimit.Cmp(deserialized.GasLimit)) + assert.Equal(transaction.To, deserialized.To) + assert.Equal(0, transaction.Value.Cmp(deserialized.Value)) + assert.Equal(transaction.Data, deserialized.Data) + assert.Equal(transaction.V, deserialized.V) + assert.Equal(transaction.R, deserialized.R) + assert.Equal(transaction.S, deserialized.S) + assert.Equal(transaction.Hash, deserialized.Hash) + assert.Equal(transaction.SenderPublicKey, deserialized.SenderPublicKey) + assert.Equal(transaction.From, deserialized.From) + + deserializedVerified, err := deserialized.Verify() + require.NoError(err) + assert.True(deserializedVerified) +} + +// TestTransactionVerifyFailsForWrongSigner confirms Verify rejects a +// signature that does not match the claimed SenderPublicKey — the negative +// case a signature-verification pipeline must reliably catch. +func TestTransactionVerifyFailsForWrongSigner(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + transaction := NewTransaction() + transaction.To = ContractConsensus + transaction.Data = AbiEncodeFunctionCall(AbiSignatureResignValidator) + + require.NoError(transaction.Sign("the real signer's passphrase")) + + otherPublicKey, err := PublicKeyFromPassphrase("a completely different passphrase") + require.NoError(err) + transaction.SenderPublicKey = otherPublicKey.ToHex() + + verified, err := transaction.Verify() + require.NoError(err) + assert.False(verified) +} + +// TestTransactionSerializeUnsignedMatchesSigningHash confirms that before a +// transaction is signed (no R/S set), Serialize(false) and Serialize(true) +// produce identical output — both must fall back to the EIP-155 +// [chainId, 0, 0] placeholder, since there is no signature yet to embed. +func TestTransactionSerializeUnsignedMatchesSigningHash(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + transaction := NewTransaction() + transaction.To = ContractUsernames + transaction.Data = AbiEncodeFunctionCall(AbiSignatureResignUsername) + + withSignature, err := transaction.Serialize(false) + require.NoError(err) + + skipSignature, err := transaction.Serialize(true) + require.NoError(err) + + assert.Equal(withSignature, skipSignature) +} + +func TestDeserializeTransactionRejectsTruncatedData(t *testing.T) { + assert := assert.New(t) + + // A valid RLP list containing a single short string — nowhere near the 9 + // fields [nonce, gasPrice, gasLimit, to, value, data, v, r, s] a + // transaction needs. + shortList := "c3820102" + + _, err := DeserializeTransaction(shortList) + assert.Error(err) +} diff --git a/crypto/transaction_types.go b/crypto/transaction_types.go new file mode 100644 index 0000000..73f2078 --- /dev/null +++ b/crypto/transaction_types.go @@ -0,0 +1,211 @@ +package crypto + +import ( + "bytes" + "encoding/hex" + "math/big" +) + +// Deliberately unlike php-crypto (which silently swallows any decode error +// and falls through to the next candidate, even after a selector match): +// once transaction.Data's leading 4 bytes match a known function's selector, +// any further decode failure is treated as a genuinely malformed transaction +// of that kind and returned as an error, rather than silently ignored. +func DecodeTransactionArgs(transaction *Transaction) error { + type candidate struct { + signature string + argCount int + apply func(*Transaction, *AbiDecoder) error + } + + candidates := []candidate{ + {AbiSignatureVote, 1, applyVote}, + {AbiSignatureUnvote, 0, applyUnvote}, + {AbiSignatureRegisterValidator, 2, applyValidatorRegistration}, + {AbiSignatureResignValidator, 0, applyValidatorResignation}, + {AbiSignatureUpdateValidator, 2, applyValidatorUpdate}, + {AbiSignatureRegisterUsername, 1, applyUsernameRegistration}, + {AbiSignatureResignUsername, 0, applyUsernameResignation}, + {AbiSignatureMultipayment, 2, applyMultiPayment}, + } + + for _, c := range candidates { + if !dataHasSelector(transaction.Data, c.signature) { + continue + } + + decoder, err := NewAbiDecoder(transaction.Data, c.signature, c.argCount) + if err != nil { + return err + } + + return c.apply(transaction, decoder) + } + + return nil +} + +func dataHasSelector(data []byte, signature string) bool { + if len(data) < abiSelectorLength { + return false + } + return bytes.Equal(data[:abiSelectorLength], AbiFunctionSelector(signature)) +} + +func applyVote(transaction *Transaction, decoder *AbiDecoder) error { + vote, err := decoder.Address(0) + if err != nil { + return err + } + transaction.Vote = vote + return nil +} + +func applyUnvote(transaction *Transaction, _ *AbiDecoder) error { + return nil +} + +func applyValidatorRegistration(transaction *Transaction, decoder *AbiDecoder) error { + pubKey, err := decoder.Bytes(0) + if err != nil { + return err + } + proof, err := decoder.Bytes(1) + if err != nil { + return err + } + transaction.ValidatorPublicKey = hex.EncodeToString(pubKey) + transaction.ValidatorProof = hex.EncodeToString(proof) + return nil +} + +func applyValidatorResignation(transaction *Transaction, _ *AbiDecoder) error { + return nil +} + +func applyValidatorUpdate(transaction *Transaction, decoder *AbiDecoder) error { + pubKey, err := decoder.Bytes(0) + if err != nil { + return err + } + proof, err := decoder.Bytes(1) + if err != nil { + return err + } + transaction.ValidatorPublicKey = hex.EncodeToString(pubKey) + transaction.ValidatorProof = hex.EncodeToString(proof) + return nil +} + +func applyUsernameRegistration(transaction *Transaction, decoder *AbiDecoder) error { + username, err := decoder.String(0) + if err != nil { + return err + } + transaction.Username = username + return nil +} + +func applyUsernameResignation(transaction *Transaction, _ *AbiDecoder) error { + return nil +} + +func applyMultiPayment(transaction *Transaction, decoder *AbiDecoder) error { + addresses, err := decoder.AddressArray(0) + if err != nil { + return err + } + amounts, err := decoder.Uint256Array(1) + if err != nil { + return err + } + transaction.PaymentAddresses = addresses + transaction.PaymentAmounts = amounts + return nil +} + +//////////////////////////////////////////////////////////////////////////////// +// TRANSACTION TYPE IDENTIFIER ///////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +// +// The functions below mirror typescript-crypto's TransactionTypeIdentifier: +// standalone, public, stateless predicates over raw calldata, computed fresh +// on every call — no stored/cached classification anywhere. This is the +// pattern actually exercised by real consumers of the sibling SDKs (e.g. +// arkvault calls TransactionTypeIdentifier.isTokenTransfer(...) directly on +// data it already has), as opposed to the full class-based Deserializer +// dispatch, which nothing outside the SDKs themselves calls. +// +// IsTransfer matches typescript-crypto's own rule (empty calldata), which is +// a different check than the value-based rule Deserializer.deserialize uses +// internally (value != 0) — that inconsistency exists in the reference +// implementation itself, not introduced here. + +func IsTransfer(data []byte) bool { + return len(data) == 0 +} + +func IsVote(data []byte) bool { + return dataHasSelector(data, AbiSignatureVote) +} + +func IsUnvote(data []byte) bool { + return dataHasSelector(data, AbiSignatureUnvote) +} + +func IsMultiPayment(data []byte) bool { + return dataHasSelector(data, AbiSignatureMultipayment) +} + +func IsUsernameRegistration(data []byte) bool { + return dataHasSelector(data, AbiSignatureRegisterUsername) +} + +func IsUsernameResignation(data []byte) bool { + return dataHasSelector(data, AbiSignatureResignUsername) +} + +func IsValidatorRegistration(data []byte) bool { + return dataHasSelector(data, AbiSignatureRegisterValidator) +} + +func IsValidatorResignation(data []byte) bool { + return dataHasSelector(data, AbiSignatureResignValidator) +} + +func IsUpdateValidator(data []byte) bool { + return dataHasSelector(data, AbiSignatureUpdateValidator) +} + +func IsTokenTransfer(data []byte) bool { + return dataHasSelector(data, AbiSignatureERC20Transfer) +} + +func IsBatchTransfer(data []byte) bool { + _, err := NewAbiDecoder(data, AbiSignatureERC20BatchTransferFrom, 3) + return err == nil +} + +// IsApprove and IsRevoke both match approve(address,uint256); only the +// decoded amount (positive vs zero) tells them apart. +func IsApprove(data []byte) bool { + amount, ok := decodedApproveAmount(data) + return ok && amount.Sign() > 0 +} + +func IsRevoke(data []byte) bool { + amount, ok := decodedApproveAmount(data) + return ok && amount.Sign() == 0 +} + +func decodedApproveAmount(data []byte) (amount *big.Int, ok bool) { + decoder, err := NewAbiDecoder(data, AbiSignatureERC20Approve, 2) + if err != nil { + return nil, false + } + amount, err = decoder.Uint256(1) + if err != nil { + return nil, false + } + return amount, true +} diff --git a/crypto/transaction_types_test.go b/crypto/transaction_types_test.go new file mode 100644 index 0000000..c9b3247 --- /dev/null +++ b/crypto/transaction_types_test.go @@ -0,0 +1,242 @@ +// This file is part of Ark Go Crypto. +// +// (c) Ark Ecosystem +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +package crypto + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mustAbiAddress(t *testing.T, address string) AbiArg { + t.Helper() + arg, err := AbiAddress(address) + require.New(t).NoError(err) + return arg +} + +func mustAbiUint256(t *testing.T, value int64) AbiArg { + t.Helper() + arg, err := AbiUint256(big.NewInt(value)) + require.New(t).NoError(err) + return arg +} + +func mustAbiAddressArray(t *testing.T, addresses []string) AbiArg { + t.Helper() + arg, err := AbiAddressArray(addresses) + require.New(t).NoError(err) + return arg +} + +func mustAbiUint256Array(t *testing.T, values ...int64) AbiArg { + t.Helper() + bigValues := make([]*big.Int, len(values)) + for i, v := range values { + bigValues[i] = big.NewInt(v) + } + arg, err := AbiUint256Array(bigValues) + require.New(t).NoError(err) + return arg +} + +func TestIsTransfer(t *testing.T) { + assert := assert.New(t) + + assert.True(IsTransfer(nil)) + assert.True(IsTransfer([]byte{})) + assert.False(IsTransfer(AbiEncodeFunctionCall(AbiSignatureUnvote))) +} + +func TestIsVote(t *testing.T) { + assert := assert.New(t) + + voteData := AbiEncodeFunctionCall(AbiSignatureVote, mustAbiAddress(t, testAddress(0x01))) + + assert.True(IsVote(voteData)) + assert.False(IsVote(AbiEncodeFunctionCall(AbiSignatureUnvote))) + assert.False(IsVote(nil)) +} + +func TestIsUnvote(t *testing.T) { + assert := assert.New(t) + + assert.True(IsUnvote(AbiEncodeFunctionCall(AbiSignatureUnvote))) + assert.False(IsUnvote(AbiEncodeFunctionCall(AbiSignatureVote, mustAbiAddress(t, testAddress(0x01))))) +} + +func TestIsMultiPayment(t *testing.T) { + assert := assert.New(t) + + data := AbiEncodeFunctionCall(AbiSignatureMultipayment, + mustAbiAddressArray(t, []string{testAddress(0x01)}), + mustAbiUint256Array(t, 100), + ) + + assert.True(IsMultiPayment(data)) + assert.False(IsMultiPayment(AbiEncodeFunctionCall(AbiSignatureVote, mustAbiAddress(t, testAddress(0x01))))) +} + +func TestIsUsernameRegistration(t *testing.T) { + assert := assert.New(t) + + data := AbiEncodeFunctionCall(AbiSignatureRegisterUsername, AbiString("test_user")) + + assert.True(IsUsernameRegistration(data)) + assert.False(IsUsernameRegistration(AbiEncodeFunctionCall(AbiSignatureResignUsername))) +} + +func TestIsUsernameResignation(t *testing.T) { + assert := assert.New(t) + + assert.True(IsUsernameResignation(AbiEncodeFunctionCall(AbiSignatureResignUsername))) + assert.False(IsUsernameResignation(AbiEncodeFunctionCall(AbiSignatureRegisterUsername, AbiString("test_user")))) +} + +// TestIsValidatorRegistrationAndIsUpdateValidatorDoNotCrossMatch exercises +// exactly the ambiguous pair discussed at length while designing this file: +// registerValidator(bytes,bytes) and updateValidator(bytes,bytes) share an +// identical argument shape, and here even identical argument *values* — only +// the selector differs, and that's the only thing these predicates may key +// off of. +func TestIsValidatorRegistrationAndIsUpdateValidatorDoNotCrossMatch(t *testing.T) { + assert := assert.New(t) + + pubKey := AbiBytes([]byte("a public key")) + proof := AbiBytes([]byte("a proof")) + + registrationData := AbiEncodeFunctionCall(AbiSignatureRegisterValidator, pubKey, proof) + updateData := AbiEncodeFunctionCall(AbiSignatureUpdateValidator, pubKey, proof) + + assert.True(IsValidatorRegistration(registrationData)) + assert.False(IsUpdateValidator(registrationData)) + + assert.True(IsUpdateValidator(updateData)) + assert.False(IsValidatorRegistration(updateData)) +} + +func TestIsValidatorResignation(t *testing.T) { + assert := assert.New(t) + + assert.True(IsValidatorResignation(AbiEncodeFunctionCall(AbiSignatureResignValidator))) + assert.False(IsValidatorResignation(AbiEncodeFunctionCall(AbiSignatureResignUsername))) +} + +func TestIsTokenTransfer(t *testing.T) { + assert := assert.New(t) + + data := AbiEncodeFunctionCall(AbiSignatureERC20Transfer, mustAbiAddress(t, testAddress(0x01)), mustAbiUint256(t, 100)) + + assert.True(IsTokenTransfer(data)) + assert.False(IsTokenTransfer(AbiEncodeFunctionCall(AbiSignatureVote, mustAbiAddress(t, testAddress(0x01))))) +} + +func TestIsBatchTransfer(t *testing.T) { + assert := assert.New(t) + + data := AbiEncodeFunctionCall(AbiSignatureERC20BatchTransferFrom, + mustAbiAddress(t, testAddress(0x01)), + mustAbiAddressArray(t, []string{testAddress(0x02)}), + mustAbiUint256Array(t, 100), + ) + + assert.True(IsBatchTransfer(data)) + assert.False(IsBatchTransfer(AbiEncodeFunctionCall(AbiSignatureVote, mustAbiAddress(t, testAddress(0x01))))) +} + +// TestIsApproveAndIsRevoke covers the one predicate pair that shares a +// selector AND an argument shape (approve(address,uint256)) — the decoded +// amount is the only thing that tells them apart. +func TestIsApproveAndIsRevoke(t *testing.T) { + assert := assert.New(t) + + approveData := AbiEncodeFunctionCall(AbiSignatureERC20Approve, mustAbiAddress(t, testAddress(0x01)), mustAbiUint256(t, 500)) + revokeData := AbiEncodeFunctionCall(AbiSignatureERC20Approve, mustAbiAddress(t, testAddress(0x01)), mustAbiUint256(t, 0)) + + assert.True(IsApprove(approveData)) + assert.False(IsRevoke(approveData)) + + assert.True(IsRevoke(revokeData)) + assert.False(IsApprove(revokeData)) +} + +// TestIsFunctionsHandleMalformedDataWithoutPanicking confirms every Is* +// predicate degrades to false on garbage input rather than panicking — these +// functions are meant to be safe to call on arbitrary calldata from +// untrusted sources. +func TestIsFunctionsHandleMalformedDataWithoutPanicking(t *testing.T) { + assert := assert.New(t) + + garbage := []byte{0xde, 0xad, 0xbe, 0xef} // selector-length, matches nothing real + + assert.NotPanics(func() { + assert.False(IsVote(garbage)) + assert.False(IsUnvote(garbage)) + assert.False(IsMultiPayment(garbage)) + assert.False(IsUsernameRegistration(garbage)) + assert.False(IsUsernameResignation(garbage)) + assert.False(IsValidatorRegistration(garbage)) + assert.False(IsValidatorResignation(garbage)) + assert.False(IsUpdateValidator(garbage)) + assert.False(IsTokenTransfer(garbage)) + assert.False(IsBatchTransfer(garbage)) + assert.False(IsApprove(garbage)) + assert.False(IsRevoke(garbage)) + }) +} + +// TestDecodeTransactionArgsPopulatesSemanticFields exercises the dispatch +// directly on hand-built Data, isolated from the RLP/ECDSA layers a full +// sign→serialize→deserialize round trip would also involve. +func TestDecodeTransactionArgsPopulatesSemanticFields(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + validatorAddress := testAddress(0x01) + + transaction := &Transaction{ + Data: AbiEncodeFunctionCall(AbiSignatureVote, mustAbiAddress(t, validatorAddress)), + } + + require.NoError(DecodeTransactionArgs(transaction)) + assert.Equal(validatorAddress, transaction.Vote) +} + +// TestDecodeTransactionArgsMalformedKnownSelectorErrors confirms the +// deliberate divergence from php-crypto: once Data's leading 4 bytes match a +// known function's selector, a subsequent decode failure is a hard error, +// not a silent fallback to the next candidate. +func TestDecodeTransactionArgsMalformedKnownSelectorErrors(t *testing.T) { + assert := assert.New(t) + + transaction := NewTransaction() + transaction.To = ContractConsensus + // The correct 4-byte selector for vote(address), followed by a payload + // too short to contain the required 32-byte address argument. + transaction.Data = append(AbiFunctionSelector(AbiSignatureVote), 0x01, 0x02) + + err := DecodeTransactionArgs(transaction) + assert.Error(err) +} + +// TestDecodeTransactionArgsNoOpForUnrecognizedData confirms a transfer or +// generic contract call (no known selector) leaves every semantic field +// untouched, rather than erroring or guessing. +func TestDecodeTransactionArgsNoOpForUnrecognizedData(t *testing.T) { + assert := assert.New(t) + + transaction := &Transaction{Data: []byte{0xde, 0xad, 0xbe, 0xef}} + + assert.NoError(DecodeTransactionArgs(transaction)) + assert.Empty(transaction.Vote) + assert.Empty(transaction.Username) + assert.Empty(transaction.ValidatorPublicKey) + assert.Empty(transaction.PaymentAddresses) +} From 8f49a3abea1de6f80be04cb8455c22994fa6c063 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Fri, 24 Jul 2026 12:30:16 +0400 Subject: [PATCH 10/16] feat: add message signing --- crypto/fixtures/message.json | 8 ++++---- crypto/message.go | 23 ++++++++++------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/crypto/fixtures/message.json b/crypto/fixtures/message.json index f997b0f..2f21960 100644 --- a/crypto/fixtures/message.json +++ b/crypto/fixtures/message.json @@ -1,8 +1,8 @@ { "data": { - "publickey": "034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed192", - "signature": "cdc26c4d137dbbad22ec94fee0bb7d7c1864291aec69c5d30a2d585efb2aa5e349e5497bb94221d8394e56a04e9b39cf9960c73b8212f46d2f03597cd73ebd33", - "message": "Hello World" + "publickey": "0243333347c8cbf4e3cbc7a96964181d02a2b0c854faa2fef86b4b8d92afcf473d", + "signature": "2bdd0c58ff8a25f456065fb731c73308a25d0a09f351f23e3c7dd3882776d33d626b0cafc0b99dd7504b24f6ecd2e036a267c8e5e005f36dcbc03b2e33fa7fc31c", + "message": "Hello, world!" }, - "passphrase": "this is a top secret passphrase" + "passphrase": "found lobster oblige describe ready addict body brave live vacuum display salute lizard combine gift resemble race senior quality reunion proud tell adjust angle" } diff --git a/crypto/message.go b/crypto/message.go index db53546..3d82a68 100644 --- a/crypto/message.go +++ b/crypto/message.go @@ -1,27 +1,26 @@ -// This file is part of Ark Go Crypto. -// -// (c) Ark Ecosystem -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - package crypto import ( - "crypto/sha256" "encoding/json" + "strconv" "github.com/fatih/structs" ) +const personalMessagePrefix = "\x19Ethereum Signed Message:\n" + +func personalSignHash(message string) []byte { + prefixed := personalMessagePrefix + strconv.Itoa(len(message)) + message + return keccak256([]byte(prefixed)) +} + func SignMessage(message string, passphrase string) (*Message, error) { privateKey, err := PrivateKeyFromPassphrase(passphrase) if err != nil { return nil, err } - hash := sha256.Sum256([]byte(message)) - sig := privateKey.Sign(hash[:]) + sig := privateKey.Sign(personalSignHash(message)) return &Message{ PublicKey: HexEncode(privateKey.PublicKey.Serialize()), @@ -41,9 +40,7 @@ func (message *Message) Verify() (bool, error) { return false, err } - hash := sha256.Sum256([]byte(message.Message)) - - return publicKey.Verify(hash[:], sig) + return publicKey.Verify(personalSignHash(message.Message), sig) } func (message *Message) ToMap() map[string]interface{} { From 9834ff9920319fbd879ebe669d4badac7323b95f Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Fri, 24 Jul 2026 14:21:27 +0400 Subject: [PATCH 11/16] feat: add message signing --- crypto/builder_test.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/crypto/builder_test.go b/crypto/builder_test.go index 2901e30..06b1923 100644 --- a/crypto/builder_test.go +++ b/crypto/builder_test.go @@ -1,10 +1,3 @@ -// This file is part of Ark Go Crypto. -// -// (c) Ark Ecosystem -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - package crypto import ( From 69a72012fef7b7ac7c542af5789dc948866980ff Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Fri, 24 Jul 2026 14:27:29 +0400 Subject: [PATCH 12/16] wip --- crypto/builder.go | 10 ---------- crypto/transaction.go | 7 ------- crypto/transaction_test.go | 12 ------------ crypto/transaction_types.go | 24 ------------------------ crypto/transaction_types_test.go | 7 ------- 5 files changed, 60 deletions(-) diff --git a/crypto/builder.go b/crypto/builder.go index 977e1dd..07d1e4e 100644 --- a/crypto/builder.go +++ b/crypto/builder.go @@ -128,9 +128,6 @@ var ( usernameDoubleUnderscoreRegexp = regexp.MustCompile(`__`) ) -// validateUsername mirrors php-crypto's Helpers::isValidUsername exactly: -// 1-20 characters, lowercase letters/digits/underscore only, no leading or -// trailing underscore, no consecutive underscores. func validateUsername(username string) error { if len(username) < 1 || len(username) > 20 { return fmt.Errorf("%w: must be between 1 and 20 characters long, got %d", ErrInvalidUsername, len(username)) @@ -168,10 +165,6 @@ func BuildUsernameResignation() *Transaction { return transaction } -// addresses/amounts must be the same length, with at least one recipient. -// The transaction's native Value is set to the sum of amounts, matching -// php-crypto/typescript-crypto (the multipayment contract call is payable -// and expects the attached value to cover the total being dispersed). func BuildMultiPayment(addresses []string, amounts []*big.Int) (*Transaction, error) { if len(addresses) != len(amounts) { return nil, fmt.Errorf("crypto: multi-payment addresses and amounts must be the same length, got %d and %d", len(addresses), len(amounts)) @@ -204,8 +197,6 @@ func BuildMultiPayment(addresses []string, amounts []*big.Int) (*Transaction, er return transaction, nil } -// Used directly for calls with no dedicated builder, and as the underlying -// mechanism for BuildBatchTransfer/BuildTokenApprove/BuildTokenTransfer below. func BuildEvmCall(to string, data []byte) (*Transaction, error) { if _, err := AddressToBytes(to); err != nil { return nil, err @@ -218,7 +209,6 @@ func BuildEvmCall(to string, data []byte) (*Transaction, error) { return transaction, nil } -// recipients/amounts must be the same length, with at least one recipient. func BuildBatchTransfer(tokenAddress string, recipients []string, amounts []*big.Int) (*Transaction, error) { if len(recipients) != len(amounts) { return nil, fmt.Errorf("crypto: batch transfer recipients and amounts must be the same length, got %d and %d", len(recipients), len(amounts)) diff --git a/crypto/transaction.go b/crypto/transaction.go index ad3396f..03496e3 100644 --- a/crypto/transaction.go +++ b/crypto/transaction.go @@ -1,10 +1,3 @@ -// This file is part of Ark Go Crypto. -// -// (c) Ark Ecosystem -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - package crypto import ( diff --git a/crypto/transaction_test.go b/crypto/transaction_test.go index 89b12b5..455c930 100644 --- a/crypto/transaction_test.go +++ b/crypto/transaction_test.go @@ -8,11 +8,6 @@ import ( "github.com/stretchr/testify/require" ) -// TestTransactionSignSerializeDeserializeVerifyRoundTrip proves the full -// Mainsail envelope pipeline end to end: build a generic (EvmCall-shaped) -// transaction, sign it, serialize it, deserialize the wire bytes back, -// confirm every field survived the round trip, and confirm the recovered -// sender and the signature both verify correctly. func TestTransactionSignSerializeDeserializeVerifyRoundTrip(t *testing.T) { assert := assert.New(t) require := require.New(t) @@ -58,9 +53,6 @@ func TestTransactionSignSerializeDeserializeVerifyRoundTrip(t *testing.T) { assert.True(deserializedVerified) } -// TestTransactionVerifyFailsForWrongSigner confirms Verify rejects a -// signature that does not match the claimed SenderPublicKey — the negative -// case a signature-verification pipeline must reliably catch. func TestTransactionVerifyFailsForWrongSigner(t *testing.T) { assert := assert.New(t) require := require.New(t) @@ -80,10 +72,6 @@ func TestTransactionVerifyFailsForWrongSigner(t *testing.T) { assert.False(verified) } -// TestTransactionSerializeUnsignedMatchesSigningHash confirms that before a -// transaction is signed (no R/S set), Serialize(false) and Serialize(true) -// produce identical output — both must fall back to the EIP-155 -// [chainId, 0, 0] placeholder, since there is no signature yet to embed. func TestTransactionSerializeUnsignedMatchesSigningHash(t *testing.T) { assert := assert.New(t) require := require.New(t) diff --git a/crypto/transaction_types.go b/crypto/transaction_types.go index 73f2078..57bcb4c 100644 --- a/crypto/transaction_types.go +++ b/crypto/transaction_types.go @@ -6,11 +6,6 @@ import ( "math/big" ) -// Deliberately unlike php-crypto (which silently swallows any decode error -// and falls through to the next candidate, even after a selector match): -// once transaction.Data's leading 4 bytes match a known function's selector, -// any further decode failure is treated as a genuinely malformed transaction -// of that kind and returned as an error, rather than silently ignored. func DecodeTransactionArgs(transaction *Transaction) error { type candidate struct { signature string @@ -124,23 +119,6 @@ func applyMultiPayment(transaction *Transaction, decoder *AbiDecoder) error { return nil } -//////////////////////////////////////////////////////////////////////////////// -// TRANSACTION TYPE IDENTIFIER ///////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -// -// The functions below mirror typescript-crypto's TransactionTypeIdentifier: -// standalone, public, stateless predicates over raw calldata, computed fresh -// on every call — no stored/cached classification anywhere. This is the -// pattern actually exercised by real consumers of the sibling SDKs (e.g. -// arkvault calls TransactionTypeIdentifier.isTokenTransfer(...) directly on -// data it already has), as opposed to the full class-based Deserializer -// dispatch, which nothing outside the SDKs themselves calls. -// -// IsTransfer matches typescript-crypto's own rule (empty calldata), which is -// a different check than the value-based rule Deserializer.deserialize uses -// internally (value != 0) — that inconsistency exists in the reference -// implementation itself, not introduced here. - func IsTransfer(data []byte) bool { return len(data) == 0 } @@ -186,8 +164,6 @@ func IsBatchTransfer(data []byte) bool { return err == nil } -// IsApprove and IsRevoke both match approve(address,uint256); only the -// decoded amount (positive vs zero) tells them apart. func IsApprove(data []byte) bool { amount, ok := decodedApproveAmount(data) return ok && amount.Sign() > 0 diff --git a/crypto/transaction_types_test.go b/crypto/transaction_types_test.go index c9b3247..4ff8f13 100644 --- a/crypto/transaction_types_test.go +++ b/crypto/transaction_types_test.go @@ -1,10 +1,3 @@ -// This file is part of Ark Go Crypto. -// -// (c) Ark Ecosystem -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - package crypto import ( From ad7546c925b8093e0a946e5cef55050d55cb63af Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Fri, 24 Jul 2026 14:28:27 +0400 Subject: [PATCH 13/16] wip --- crypto/builder_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/crypto/builder_test.go b/crypto/builder_test.go index 06b1923..6f33f70 100644 --- a/crypto/builder_test.go +++ b/crypto/builder_test.go @@ -10,9 +10,6 @@ import ( const testPassphrase = "this is a top secret passphrase" -// signSerializeDeserialize signs transaction with testPassphrase, round-trips -// it through Serialize/DeserializeTransaction (which also runs -// DecodeTransactionArgs), and returns the deserialized result for assertion. func signSerializeDeserialize(t *testing.T, transaction *Transaction) *Transaction { t.Helper() require := require.New(t) From d0dc3f86072710713028978651969c62a14f24b9 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Fri, 24 Jul 2026 14:38:17 +0400 Subject: [PATCH 14/16] wip --- crypto/message_test.go | 55 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 crypto/message_test.go diff --git a/crypto/message_test.go b/crypto/message_test.go new file mode 100644 index 0000000..2a8580b --- /dev/null +++ b/crypto/message_test.go @@ -0,0 +1,55 @@ +package crypto + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSignMessageMatchesPhpTsFixture cross-checks against php-crypto's/ +// typescript-crypto's own message-sign fixture (same message and passphrase), +// proving the personal_sign hash construction is byte-identical across SDKs. +func TestSignMessageMatchesPhpTsFixture(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + fixture := GetMessageFixture() + + signed, err := SignMessage(fixture.Data.Message, fixture.Passphrase) + require.NoError(err) + + assert.Equal(fixture.Data.PublicKey, signed.PublicKey) + assert.Equal(fixture.Data.Signature, signed.Signature) +} + +func TestMessageVerify(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + fixture := GetMessageFixture() + + message := &Message{ + PublicKey: fixture.Data.PublicKey, + Signature: fixture.Data.Signature, + Message: fixture.Data.Message, + } + + verified, err := message.Verify() + require.NoError(err) + assert.True(verified) +} + +func TestMessageVerifyRejectsTamperedMessage(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + signed, err := SignMessage("original message", testPassphrase) + require.NoError(err) + + signed.Message = "tampered message" + + verified, err := signed.Verify() + require.NoError(err) + assert.False(verified) +} From 6babb29e8f1ea45cd96b2c21c023d304d1e92974 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Fri, 24 Jul 2026 15:53:10 +0400 Subject: [PATCH 15/16] wip --- crypto/builder_test.go | 3 --- crypto/structs.go | 5 +---- crypto/transaction_types_test.go | 23 ----------------------- 3 files changed, 1 insertion(+), 30 deletions(-) diff --git a/crypto/builder_test.go b/crypto/builder_test.go index 6f33f70..f647b00 100644 --- a/crypto/builder_test.go +++ b/crypto/builder_test.go @@ -289,6 +289,3 @@ func TestBuildTokenTransferRoundTrip(t *testing.T) { assert.True(IsTokenTransfer(deserialized.Data)) assert.Equal(token, deserialized.To) } - -// Dispatch/type-identifier tests (DecodeTransactionArgs, IsVote, IsUnvote, -// ...) live in transaction_types_test.go, alongside the code they cover. diff --git a/crypto/structs.go b/crypto/structs.go index ea0f4a4..0e81471 100644 --- a/crypto/structs.go +++ b/crypto/structs.go @@ -55,10 +55,7 @@ type Transaction struct { // The fields below are populated only for the transaction kind they // apply to, by DecodeTransactionArgs during deserialization; all others - // are left at their zero value. Mainsail transactions carry no explicit - // type field on the wire — use the IsVote/IsUnvote/... family of - // functions (matching typescript-crypto's TransactionTypeIdentifier) to - // check what kind of transaction Data represents. + // are left at their zero value. Vote string `json:"vote,omitempty"` ValidatorPublicKey string `json:"validatorPublicKey,omitempty"` ValidatorProof string `json:"validatorProof,omitempty"` diff --git a/crypto/transaction_types_test.go b/crypto/transaction_types_test.go index 4ff8f13..10925b6 100644 --- a/crypto/transaction_types_test.go +++ b/crypto/transaction_types_test.go @@ -93,12 +93,6 @@ func TestIsUsernameResignation(t *testing.T) { assert.False(IsUsernameResignation(AbiEncodeFunctionCall(AbiSignatureRegisterUsername, AbiString("test_user")))) } -// TestIsValidatorRegistrationAndIsUpdateValidatorDoNotCrossMatch exercises -// exactly the ambiguous pair discussed at length while designing this file: -// registerValidator(bytes,bytes) and updateValidator(bytes,bytes) share an -// identical argument shape, and here even identical argument *values* — only -// the selector differs, and that's the only thing these predicates may key -// off of. func TestIsValidatorRegistrationAndIsUpdateValidatorDoNotCrossMatch(t *testing.T) { assert := assert.New(t) @@ -144,9 +138,6 @@ func TestIsBatchTransfer(t *testing.T) { assert.False(IsBatchTransfer(AbiEncodeFunctionCall(AbiSignatureVote, mustAbiAddress(t, testAddress(0x01))))) } -// TestIsApproveAndIsRevoke covers the one predicate pair that shares a -// selector AND an argument shape (approve(address,uint256)) — the decoded -// amount is the only thing that tells them apart. func TestIsApproveAndIsRevoke(t *testing.T) { assert := assert.New(t) @@ -160,10 +151,6 @@ func TestIsApproveAndIsRevoke(t *testing.T) { assert.False(IsApprove(revokeData)) } -// TestIsFunctionsHandleMalformedDataWithoutPanicking confirms every Is* -// predicate degrades to false on garbage input rather than panicking — these -// functions are meant to be safe to call on arbitrary calldata from -// untrusted sources. func TestIsFunctionsHandleMalformedDataWithoutPanicking(t *testing.T) { assert := assert.New(t) @@ -185,9 +172,6 @@ func TestIsFunctionsHandleMalformedDataWithoutPanicking(t *testing.T) { }) } -// TestDecodeTransactionArgsPopulatesSemanticFields exercises the dispatch -// directly on hand-built Data, isolated from the RLP/ECDSA layers a full -// sign→serialize→deserialize round trip would also involve. func TestDecodeTransactionArgsPopulatesSemanticFields(t *testing.T) { assert := assert.New(t) require := require.New(t) @@ -202,10 +186,6 @@ func TestDecodeTransactionArgsPopulatesSemanticFields(t *testing.T) { assert.Equal(validatorAddress, transaction.Vote) } -// TestDecodeTransactionArgsMalformedKnownSelectorErrors confirms the -// deliberate divergence from php-crypto: once Data's leading 4 bytes match a -// known function's selector, a subsequent decode failure is a hard error, -// not a silent fallback to the next candidate. func TestDecodeTransactionArgsMalformedKnownSelectorErrors(t *testing.T) { assert := assert.New(t) @@ -219,9 +199,6 @@ func TestDecodeTransactionArgsMalformedKnownSelectorErrors(t *testing.T) { assert.Error(err) } -// TestDecodeTransactionArgsNoOpForUnrecognizedData confirms a transfer or -// generic contract call (no known selector) leaves every semantic field -// untouched, rather than erroring or guessing. func TestDecodeTransactionArgsNoOpForUnrecognizedData(t *testing.T) { assert := assert.New(t) From 167ada2903bc0f8bdb077b6aaccd53b9bd5ce32e Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Mon, 27 Jul 2026 15:29:50 +0400 Subject: [PATCH 16/16] test: tweak tests for Mainsail --- .github/workflows/test.yml | 2 +- crypto/fixtures.go | 39 +++- .../fixtures/transactions/multi_payment.json | 18 ++ .../multi-payment-multi-sign.json | 32 --- .../multi_payment/multi-payment-sign.json | 27 --- .../multi-payment-with-vendor-field-sign.json | 28 --- .../multi-signature-registration-sign.json | 30 --- crypto/fixtures/transactions/transfer.json | 18 ++ .../transfer/transfer-multi-sign.json | 22 -- .../transactions/transfer/transfer-sign.json | 17 -- .../transfer-with-vendor-field-sign.json | 18 -- crypto/fixtures/transactions/unvote.json | 18 ++ .../transactions/username_registration.json | 18 ++ .../username-registration-multi-sign.json | 23 -- .../username-registration-sign.json | 18 -- .../transactions/username_resignation.json | 18 ++ .../username-resignation-multi-sign.json | 20 -- .../username-resignation-sign.json | 15 -- .../transactions/validator_registration.json | 18 ++ .../validator-registration-multi-sign.json | 23 -- .../validator-registration-sign.json | 18 -- .../transactions/validator_resignation.json | 18 ++ .../validator-resignation-multi-sign.json | 20 -- .../validator-resignation-sign.json | 15 -- .../transactions/validator_update.json | 18 ++ crypto/fixtures/transactions/vote.json | 18 ++ .../transactions/vote/vote-multi-sign.json | 25 --- .../fixtures/transactions/vote/vote-sign.json | 20 -- crypto/serdeser_test.go | 200 ++++++++++++++++++ 29 files changed, 393 insertions(+), 381 deletions(-) create mode 100644 crypto/fixtures/transactions/multi_payment.json delete mode 100644 crypto/fixtures/transactions/multi_payment/multi-payment-multi-sign.json delete mode 100644 crypto/fixtures/transactions/multi_payment/multi-payment-sign.json delete mode 100644 crypto/fixtures/transactions/multi_payment/multi-payment-with-vendor-field-sign.json delete mode 100644 crypto/fixtures/transactions/multi_signature_registration/multi-signature-registration-sign.json create mode 100644 crypto/fixtures/transactions/transfer.json delete mode 100644 crypto/fixtures/transactions/transfer/transfer-multi-sign.json delete mode 100644 crypto/fixtures/transactions/transfer/transfer-sign.json delete mode 100644 crypto/fixtures/transactions/transfer/transfer-with-vendor-field-sign.json create mode 100644 crypto/fixtures/transactions/unvote.json create mode 100644 crypto/fixtures/transactions/username_registration.json delete mode 100644 crypto/fixtures/transactions/username_registration/username-registration-multi-sign.json delete mode 100644 crypto/fixtures/transactions/username_registration/username-registration-sign.json create mode 100644 crypto/fixtures/transactions/username_resignation.json delete mode 100644 crypto/fixtures/transactions/username_resignation/username-resignation-multi-sign.json delete mode 100644 crypto/fixtures/transactions/username_resignation/username-resignation-sign.json create mode 100644 crypto/fixtures/transactions/validator_registration.json delete mode 100644 crypto/fixtures/transactions/validator_registration/validator-registration-multi-sign.json delete mode 100644 crypto/fixtures/transactions/validator_registration/validator-registration-sign.json create mode 100644 crypto/fixtures/transactions/validator_resignation.json delete mode 100644 crypto/fixtures/transactions/validator_resignation/validator-resignation-multi-sign.json delete mode 100644 crypto/fixtures/transactions/validator_resignation/validator-resignation-sign.json create mode 100644 crypto/fixtures/transactions/validator_update.json create mode 100644 crypto/fixtures/transactions/vote.json delete mode 100644 crypto/fixtures/transactions/vote/vote-multi-sign.json delete mode 100644 crypto/fixtures/transactions/vote/vote-sign.json create mode 100644 crypto/serdeser_test.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 66bf5dc..9fecebc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.23.x" + go-version: "1.25.x" id: go - name: Check out code into the Go module directory diff --git a/crypto/fixtures.go b/crypto/fixtures.go index b722968..cb822c9 100644 --- a/crypto/fixtures.go +++ b/crypto/fixtures.go @@ -1,21 +1,14 @@ -// This file is part of Ark Go Crypto. -// -// (c) Ark Ecosystem -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - package crypto import ( "encoding/json" "fmt" - "io/ioutil" "log" + "os" ) func GetFile(path string) string { - data, err := ioutil.ReadFile(path) + data, err := os.ReadFile(path) if err != nil { log.Fatalf("Cannot read file %s: %s", path, err) @@ -64,6 +57,15 @@ func GetBLSKeysFixture() []BLSKeyFixture { return fixtures } +func GetTransactionFixture(name string) TestingTransactionFixture { + data := GetFile(fmt.Sprintf("./fixtures/transactions/%s.json", name)) + + var fixture TestingTransactionFixture + _ = json.Unmarshal([]byte(data), &fixture) + + return fixture +} + type TestingIdentityFixture struct { Data struct { PrivateKey string `json:"privateKey,omitempty"` @@ -93,3 +95,22 @@ type BLSKeyFixture struct { BLSPrivateKey string `json:"bls_private_key"` Passphrase string `json:"passphrase"` } + +type TestingTransactionFixture struct { + Data struct { + Nonce string `json:"nonce"` + GasPrice string `json:"gasPrice"` + GasLimit string `json:"gasLimit"` + To string `json:"to"` + Value string `json:"value"` + Data string `json:"data"` + Network int `json:"network"` + V int `json:"v"` + R string `json:"r"` + S string `json:"s"` + SenderPublicKey string `json:"senderPublicKey"` + From string `json:"from"` + Hash string `json:"hash"` + } `json:"data"` + Serialized string `json:"serialized"` +} diff --git a/crypto/fixtures/transactions/multi_payment.json b/crypto/fixtures/transactions/multi_payment.json new file mode 100644 index 0000000..cfce43b --- /dev/null +++ b/crypto/fixtures/transactions/multi_payment.json @@ -0,0 +1,18 @@ +{ + "data": { + "nonce": "1", + "gasPrice": "5000000000", + "gasLimit": "200000", + "to": "0x00EFd0D4639191C49908A7BddbB9A11A994A8527", + "value": "300000", + "data": "084ce708000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000006f0182a0cc707b055322ccf6d4cb6a5aff1aeb22000000000000000000000000c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000186a00000000000000000000000000000000000000000000000000000000000030d40", + "network": 11812, + "v": 1, + "r": "9720cd5959af4fc513ec8f9c55a3f3b53a1f24a3a577e8132cadd81c116c6c82", + "s": "3411570db14d5e1a6673b2e80fe6cc22bf67752808ac4752c8bcfd9732e33dc8", + "senderPublicKey": "0243333347c8cbf4e3cbc7a96964181d02a2b0c854faa2fef86b4b8d92afcf473d", + "from": "0x1E6747BEAa5B4076a6A98D735DF8c35a70D18Bdd", + "hash": "f603e0b5fe0743021d2879ffe53ace7b40c9d823edbf058bd25a045753c82aa9" + }, + "serialized": "f901700185012a05f20083030d409400efd0d4639191c49908a7bddbb9a11a994a8527830493e0b90104084ce708000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000006f0182a0cc707b055322ccf6d4cb6a5aff1aeb22000000000000000000000000c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000186a00000000000000000000000000000000000000000000000000000000000030d40825c6ca09720cd5959af4fc513ec8f9c55a3f3b53a1f24a3a577e8132cadd81c116c6c82a03411570db14d5e1a6673b2e80fe6cc22bf67752808ac4752c8bcfd9732e33dc8" +} \ No newline at end of file diff --git a/crypto/fixtures/transactions/multi_payment/multi-payment-multi-sign.json b/crypto/fixtures/transactions/multi_payment/multi-payment-multi-sign.json deleted file mode 100644 index d082f0d..0000000 --- a/crypto/fixtures/transactions/multi_payment/multi-payment-multi-sign.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "transaction": { - "version": 1, - "network": 30, - "typeGroup": 1, - "type": 6, - "nonce": "0", - "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", - "fee": "10000000", - "amount": "3", - "asset": { - "payments": [ - { - "amount": "1", - "recipientId": "0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A" - }, - { - "amount": "2", - "recipientId": "0x27FA7CaFFaAE77dDb9AB232FDBDa56D5e5Af2393" - } - ] - }, - "signature": "cf4e94776e768110e62961747f39b68993d4ff5ec2171cad2fab6d77babb2ce31181f005876031d43e6d7732770c6aca73b33057c7aad6279cf2da03829a7b62", - "signatures": [ - "006581c24bbe49e57127604b18d2efdd8d1d2bfe23f9b1e7f15b3f46a647b976e79884b61331546c5844ded781974e181a03a4066f858020fb347ad9628773b465", - "011fb86a8ddbddbed012ff46fc0f3c9a30f3c1544d7a32b9355c318ad453837615e8399f676856c97cf6dcb8d18507ff9c0ffe195d23624e296182477644ed3689", - "023aa80e3663d737716818978ffbae59a8a9f32257a341e1391ca1e0fd744f3382b41e507dca92fa91f98e2024b67938420aaf0c268266dfe4d1f92e81cb3a0421" - ], - "id": "5349b6d57228cb2aeb77e198d6adc93f745e3abb9b4624929b7e29f4a920c0a5" - }, - "serializedHex": "ff011e0100000006000000000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d380969800000000000002000100000000000000b693449adda7efc015d87944eae8b7c37eb1690a020000000000000027fa7caffaae77ddb9ab232fdbda56d5e5af2393cf4e94776e768110e62961747f39b68993d4ff5ec2171cad2fab6d77babb2ce31181f005876031d43e6d7732770c6aca73b33057c7aad6279cf2da03829a7b62006581c24bbe49e57127604b18d2efdd8d1d2bfe23f9b1e7f15b3f46a647b976e79884b61331546c5844ded781974e181a03a4066f858020fb347ad9628773b465011fb86a8ddbddbed012ff46fc0f3c9a30f3c1544d7a32b9355c318ad453837615e8399f676856c97cf6dcb8d18507ff9c0ffe195d23624e296182477644ed3689023aa80e3663d737716818978ffbae59a8a9f32257a341e1391ca1e0fd744f3382b41e507dca92fa91f98e2024b67938420aaf0c268266dfe4d1f92e81cb3a0421" -} diff --git a/crypto/fixtures/transactions/multi_payment/multi-payment-sign.json b/crypto/fixtures/transactions/multi_payment/multi-payment-sign.json deleted file mode 100644 index b28d7f7..0000000 --- a/crypto/fixtures/transactions/multi_payment/multi-payment-sign.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "transaction": { - "version": 1, - "network": 30, - "typeGroup": 1, - "type": 6, - "nonce": "0", - "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", - "fee": "10000000", - "amount": "3", - "asset": { - "payments": [ - { - "amount": "1", - "recipientId": "0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A" - }, - { - "amount": "2", - "recipientId": "0x27FA7CaFFaAE77dDb9AB232FDBDa56D5e5Af2393" - } - ] - }, - "signature": "e782cc5a7622ea23463aa3d6795850b7163abf6b02c29795f9230f4b0537404cbccf82de6a44ecf6e98391bff601f033524c8704f4081ada4f72e98aea5ae173", - "id": "c820af7342cde48a2d578e6324329fc557ffb4e40fa3208b348cf07fd2020e43" - }, - "serializedHex": "ff011e0100000006000000000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d380969800000000000002000100000000000000b693449adda7efc015d87944eae8b7c37eb1690a020000000000000027fa7caffaae77ddb9ab232fdbda56d5e5af2393e782cc5a7622ea23463aa3d6795850b7163abf6b02c29795f9230f4b0537404cbccf82de6a44ecf6e98391bff601f033524c8704f4081ada4f72e98aea5ae173" -} diff --git a/crypto/fixtures/transactions/multi_payment/multi-payment-with-vendor-field-sign.json b/crypto/fixtures/transactions/multi_payment/multi-payment-with-vendor-field-sign.json deleted file mode 100644 index 7c8a631..0000000 --- a/crypto/fixtures/transactions/multi_payment/multi-payment-with-vendor-field-sign.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "transaction": { - "version": 1, - "network": 30, - "typeGroup": 1, - "type": 6, - "nonce": "0", - "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", - "fee": "10000000", - "amount": "3", - "vendorField": "this is a top secret vendor field", - "asset": { - "payments": [ - { - "amount": "1", - "recipientId": "0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A" - }, - { - "amount": "2", - "recipientId": "0x27FA7CaFFaAE77dDb9AB232FDBDa56D5e5Af2393" - } - ] - }, - "signature": "ff68aa386548aba97004d99d616d5bdd1f13074dd85fdb0ab636d18585d4436030f66f42fa257c2b18873a076906af31a2b3b838081d9ec79f685f32d4640955", - "id": "4c759d085999e25996e2af264c79aac76c44e48d3f5f2432c56d8a2e4fd99349" - }, - "serializedHex": "ff011e0100000006000000000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d380969800000000002174686973206973206120746f70207365637265742076656e646f72206669656c6402000100000000000000b693449adda7efc015d87944eae8b7c37eb1690a020000000000000027fa7caffaae77ddb9ab232fdbda56d5e5af2393ff68aa386548aba97004d99d616d5bdd1f13074dd85fdb0ab636d18585d4436030f66f42fa257c2b18873a076906af31a2b3b838081d9ec79f685f32d4640955" -} diff --git a/crypto/fixtures/transactions/multi_signature_registration/multi-signature-registration-sign.json b/crypto/fixtures/transactions/multi_signature_registration/multi-signature-registration-sign.json deleted file mode 100644 index 7917664..0000000 --- a/crypto/fixtures/transactions/multi_signature_registration/multi-signature-registration-sign.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "transaction": { - "version": 1, - "network": 30, - "typeGroup": 1, - "type": 4, - "nonce": "2", - "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", - "fee": "500000000", - "amount": "0", - "asset": { - "multiSignature": { - "min": 2, - "publicKeys": [ - "029fab3cb2f5e248ae7cbb4de646741da4d73c493b2a03ab5c71507fb2c0dcca92", - "03629f9dbf7f1e91cefa845126189816ceae357bdd1f41bd14787318a7d5b55d48", - "027941d2059f89a26d89e87d3385e261a0ede1234aaeaa487012b69d6b67962dc5" - ] - } - }, - "signature": "2b33558cdc62933ff56feb646d1f47a98104bf34b894895d5e816f86e556f87fce8485e55aa32dfa1cd86456a66a58ef7a68dff4af51e2f7fcf75b983540872e", - "signatures": [ - "000caa6864c71362b369c71107f463f29c43c361e54260cbc54d791b7385dbe76f29d12b9befbe4f98792d7046481afcf0c156408310192a93d8413e5380438f27", - "0153a22c5ce2b1894f0a141adc19de567077d2d268f4c7e1476e9558ecb4411d486cd0d7aa3e9c7716739a055a8a1a64a162d0362645d63d13791a9876ef8b5a88", - "021d56997f0c9e21201c59e1b7b6be8d5a609908a4f65bf266144baf5e61e3f14bc2651f62187f4ffa17c8b010fcb0fba94a04df56bc25e5cb20935ec5fb7ad632" - ], - "id": "1941926b880ab606633b3a2361df784f6085ded8b5d3cf52e94998e1468b3197" - }, - "serializedHex": "ff011e0100000004000200000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d30065cd1d00000000000203029fab3cb2f5e248ae7cbb4de646741da4d73c493b2a03ab5c71507fb2c0dcca9203629f9dbf7f1e91cefa845126189816ceae357bdd1f41bd14787318a7d5b55d48027941d2059f89a26d89e87d3385e261a0ede1234aaeaa487012b69d6b67962dc52b33558cdc62933ff56feb646d1f47a98104bf34b894895d5e816f86e556f87fce8485e55aa32dfa1cd86456a66a58ef7a68dff4af51e2f7fcf75b983540872e000caa6864c71362b369c71107f463f29c43c361e54260cbc54d791b7385dbe76f29d12b9befbe4f98792d7046481afcf0c156408310192a93d8413e5380438f270153a22c5ce2b1894f0a141adc19de567077d2d268f4c7e1476e9558ecb4411d486cd0d7aa3e9c7716739a055a8a1a64a162d0362645d63d13791a9876ef8b5a88021d56997f0c9e21201c59e1b7b6be8d5a609908a4f65bf266144baf5e61e3f14bc2651f62187f4ffa17c8b010fcb0fba94a04df56bc25e5cb20935ec5fb7ad632" -} diff --git a/crypto/fixtures/transactions/transfer.json b/crypto/fixtures/transactions/transfer.json new file mode 100644 index 0000000..6fe549e --- /dev/null +++ b/crypto/fixtures/transactions/transfer.json @@ -0,0 +1,18 @@ +{ + "data": { + "nonce": "1", + "gasPrice": "5000000000", + "gasLimit": "21000", + "to": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", + "value": "100000000", + "data": "", + "network": 11812, + "v": 0, + "r": "a1f79cb40a4bb409d6cebd874002ceda3ec0ccb614c1d8155f5c2f7f798135f9", + "s": "2d2ef517aaf6feed747385e260c206f46b2ce9d6b2a585427a111685a097bd79", + "senderPublicKey": "0243333347c8cbf4e3cbc7a96964181d02a2b0c854faa2fef86b4b8d92afcf473d", + "from": "0x1E6747BEAa5B4076a6A98D735DF8c35a70D18Bdd", + "hash": "a39435ec5de418e77479856d06a653efc171afe43e091472af22ee359eeb83be" + }, + "serialized": "f86a0185012a05f200825208946f0182a0cc707b055322ccf6d4cb6a5aff1aeb228405f5e10080825c6ba0a1f79cb40a4bb409d6cebd874002ceda3ec0ccb614c1d8155f5c2f7f798135f9a02d2ef517aaf6feed747385e260c206f46b2ce9d6b2a585427a111685a097bd79" +} \ No newline at end of file diff --git a/crypto/fixtures/transactions/transfer/transfer-multi-sign.json b/crypto/fixtures/transactions/transfer/transfer-multi-sign.json deleted file mode 100644 index bc98998..0000000 --- a/crypto/fixtures/transactions/transfer/transfer-multi-sign.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "transaction": { - "version": 1, - "network": 30, - "typeGroup": 1, - "type": 0, - "nonce": "1", - "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", - "fee": "10000000", - "amount": "1", - "expiration": 0, - "recipientId": "0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A", - "signature": "f25db2b781b79f671b7848284de63f3cb898f9938c0b6203a11cace4aaa4b962eabe9c4a67c207a3f8baea3b4f4cef90a0b67a2bfd84cd61dcc771597f52656d", - "signatures": [ - "005afa0050c85a2ac9b34accb0f47c6a9cc9eee831ac713e62e846898d1b75d6a33034680bce95f638c6dfabf8056f8afa9ef73a3c35741234faf01d0a346e3e7d", - "0104bd019e5a6ea9ee5cc41d9f39efe2a2df2cc653369fdfcb56d5219fa282a8368067586748feb2483883c9eca50a5ae73abd7139d4d1885914ed9d5e5c63f8fb", - "02d6af0f5a85a7967d677b2f1f86e00b8ca37facb714467e12810a692d5bcbdfcac4e4c2d4b79a70c7366405339bf84a854308d20cb48652816a9cf37fb3e86e00" - ], - "id": "dcb9d29590313cf6e1b53f120e621e34c39e45fce9114272158036cb99be9c89" - }, - "serializedHex": "ff011e0100000000000100000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3809698000000000000010000000000000000000000b693449adda7efc015d87944eae8b7c37eb1690af25db2b781b79f671b7848284de63f3cb898f9938c0b6203a11cace4aaa4b962eabe9c4a67c207a3f8baea3b4f4cef90a0b67a2bfd84cd61dcc771597f52656d005afa0050c85a2ac9b34accb0f47c6a9cc9eee831ac713e62e846898d1b75d6a33034680bce95f638c6dfabf8056f8afa9ef73a3c35741234faf01d0a346e3e7d0104bd019e5a6ea9ee5cc41d9f39efe2a2df2cc653369fdfcb56d5219fa282a8368067586748feb2483883c9eca50a5ae73abd7139d4d1885914ed9d5e5c63f8fb02d6af0f5a85a7967d677b2f1f86e00b8ca37facb714467e12810a692d5bcbdfcac4e4c2d4b79a70c7366405339bf84a854308d20cb48652816a9cf37fb3e86e00" -} diff --git a/crypto/fixtures/transactions/transfer/transfer-sign.json b/crypto/fixtures/transactions/transfer/transfer-sign.json deleted file mode 100644 index 2a59087..0000000 --- a/crypto/fixtures/transactions/transfer/transfer-sign.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "transaction": { - "version": 1, - "network": 30, - "typeGroup": 1, - "type": 0, - "nonce": "1", - "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", - "fee": "10000000", - "amount": "1", - "expiration": 0, - "recipientId": "0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A", - "signature": "97dd98ed1066aab76011114cbf5ac00d741812cac326cda7966e1af08874d7212f350d769221d2a78ea185d77ac4e2c5c718e4a84b1a1ddcbf504d642eb94b6a", - "id": "e20265dba26594c4202e224e8d1069dfae840a8db7cc6acfafefa2b3b02787e5" - }, - "serializedHex": "ff011e0100000000000100000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3809698000000000000010000000000000000000000b693449adda7efc015d87944eae8b7c37eb1690a97dd98ed1066aab76011114cbf5ac00d741812cac326cda7966e1af08874d7212f350d769221d2a78ea185d77ac4e2c5c718e4a84b1a1ddcbf504d642eb94b6a" -} diff --git a/crypto/fixtures/transactions/transfer/transfer-with-vendor-field-sign.json b/crypto/fixtures/transactions/transfer/transfer-with-vendor-field-sign.json deleted file mode 100644 index e520639..0000000 --- a/crypto/fixtures/transactions/transfer/transfer-with-vendor-field-sign.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "transaction": { - "version": 1, - "network": 30, - "typeGroup": 1, - "type": 0, - "nonce": "1", - "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", - "fee": "10000000", - "amount": "1", - "vendorField": "this is a top secret vendor field", - "expiration": 0, - "recipientId": "0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A", - "signature": "d7062dd749406741ab848cc0bb6bcc82c04d8b46b699803154a30bda8d13bd6dc6c06fcbbb6a6edab3b83772008e50524b4563a33c0f08009b4cc72f38986981", - "id": "59cc59e3609f86418ef04fff270fb0f59ea2ff5a383857a3c157056f0925d59d" - }, - "serializedHex": "ff011e0100000000000100000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d380969800000000002174686973206973206120746f70207365637265742076656e646f72206669656c64010000000000000000000000b693449adda7efc015d87944eae8b7c37eb1690ad7062dd749406741ab848cc0bb6bcc82c04d8b46b699803154a30bda8d13bd6dc6c06fcbbb6a6edab3b83772008e50524b4563a33c0f08009b4cc72f38986981" -} diff --git a/crypto/fixtures/transactions/unvote.json b/crypto/fixtures/transactions/unvote.json new file mode 100644 index 0000000..8708e36 --- /dev/null +++ b/crypto/fixtures/transactions/unvote.json @@ -0,0 +1,18 @@ +{ + "data": { + "nonce": "1", + "gasPrice": "5000000000", + "gasLimit": "200000", + "to": "0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1", + "value": "0", + "data": "3174b689", + "network": 11812, + "v": 0, + "r": "7e05d686131973a9fbda9028f9df5702d9a229823b183caac03ec3a874ccec52", + "s": "1980fbc959612d8c7b97f6f16742259635f42a2039839a81aaaeb7ae6930a5f5", + "senderPublicKey": "0243333347c8cbf4e3cbc7a96964181d02a2b0c854faa2fef86b4b8d92afcf473d", + "from": "0x1E6747BEAa5B4076a6A98D735DF8c35a70D18Bdd", + "hash": "38d018d22e2cef185dc3c2f5d25ec63535160d8dfeaf78da8db719cb7ffc730d" + }, + "serialized": "f86b0185012a05f20083030d4094535b3d7a252fa034ed71f0c53ec0c6f784cb64e180843174b689825c6ba07e05d686131973a9fbda9028f9df5702d9a229823b183caac03ec3a874ccec52a01980fbc959612d8c7b97f6f16742259635f42a2039839a81aaaeb7ae6930a5f5" +} \ No newline at end of file diff --git a/crypto/fixtures/transactions/username_registration.json b/crypto/fixtures/transactions/username_registration.json new file mode 100644 index 0000000..afe110d --- /dev/null +++ b/crypto/fixtures/transactions/username_registration.json @@ -0,0 +1,18 @@ +{ + "data": { + "nonce": "1", + "gasPrice": "5000000000", + "gasLimit": "200000", + "to": "0x2c1DE3b4Dbb4aDebEbB5dcECAe825bE2a9fc6eb6", + "value": "0", + "data": "36a94134000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000076669787475726500000000000000000000000000000000000000000000000000", + "network": 11812, + "v": 1, + "r": "80c77145c6b5450f4805e7c7b649b49653d151ca74275991ae49731084e35049", + "s": "1a25084fb0e997f93d9cc84472281f01c3e1fdeb15c29abf24f3469aeaac87c1", + "senderPublicKey": "0243333347c8cbf4e3cbc7a96964181d02a2b0c854faa2fef86b4b8d92afcf473d", + "from": "0x1E6747BEAa5B4076a6A98D735DF8c35a70D18Bdd", + "hash": "0fd23793ced45b59f54d6250b0d74860fb5960ffc971457c349032cc33cf560a" + }, + "serialized": "f8cc0185012a05f20083030d40942c1de3b4dbb4adebebb5dcecae825be2a9fc6eb680b86436a94134000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000076669787475726500000000000000000000000000000000000000000000000000825c6ca080c77145c6b5450f4805e7c7b649b49653d151ca74275991ae49731084e35049a01a25084fb0e997f93d9cc84472281f01c3e1fdeb15c29abf24f3469aeaac87c1" +} \ No newline at end of file diff --git a/crypto/fixtures/transactions/username_registration/username-registration-multi-sign.json b/crypto/fixtures/transactions/username_registration/username-registration-multi-sign.json deleted file mode 100644 index 227356d..0000000 --- a/crypto/fixtures/transactions/username_registration/username-registration-multi-sign.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "transaction": { - "version": 1, - "network": 30, - "typeGroup": 1, - "type": 8, - "nonce": "6", - "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", - "fee": "2500000000", - "amount": "0", - "asset": { - "username": "simple_tx_tester" - }, - "signature": "9a74c959d0cd4ad4352292417de3ed8ed0cac9aad390bb5d93a5ef5d2bdbf54dad9bbf690f2079ad397141acd182b6e5c4285bd64b068083431daffd5dbc5b09", - "signatures": [ - "003b5b94d80da3d41f514d77a624f26f7a2336c34ce1ec62f06c61635db31b27135a58b9ee705cbaa45addd6e75098833c80935541ccc050199443a3d4c62c7fdd", - "01335feff7cc3d6e3524dd9892e029d2dea77c76afdeb9445bc0d14c587fd922c6c38e3542b978aeb370dc349b4a1221015f33051407f10ca051a1caa2109fc510", - "02fe8b1d0be5a77455b38f9df31a379e1a80bcab4003a225cbf79d426fcd248e03ff5f499a404d879270086e292aa45ac1af979aa9af4a64e4881ec559fc87e779" - ], - "id": "ffaead0f6003e125bb2a66169e87efe29c05dedaec71898abfcbc7a1f846f2b3" - }, - "serializedHex": "ff011e0100000008000600000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d300f9029500000000001073696d706c655f74785f7465737465729a74c959d0cd4ad4352292417de3ed8ed0cac9aad390bb5d93a5ef5d2bdbf54dad9bbf690f2079ad397141acd182b6e5c4285bd64b068083431daffd5dbc5b09003b5b94d80da3d41f514d77a624f26f7a2336c34ce1ec62f06c61635db31b27135a58b9ee705cbaa45addd6e75098833c80935541ccc050199443a3d4c62c7fdd01335feff7cc3d6e3524dd9892e029d2dea77c76afdeb9445bc0d14c587fd922c6c38e3542b978aeb370dc349b4a1221015f33051407f10ca051a1caa2109fc51002fe8b1d0be5a77455b38f9df31a379e1a80bcab4003a225cbf79d426fcd248e03ff5f499a404d879270086e292aa45ac1af979aa9af4a64e4881ec559fc87e779" -} diff --git a/crypto/fixtures/transactions/username_registration/username-registration-sign.json b/crypto/fixtures/transactions/username_registration/username-registration-sign.json deleted file mode 100644 index 5e6c1c7..0000000 --- a/crypto/fixtures/transactions/username_registration/username-registration-sign.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "transaction": { - "version": 1, - "network": 30, - "typeGroup": 1, - "type": 8, - "nonce": "4", - "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", - "fee": "2500000000", - "amount": "0", - "asset": { - "username": "simple_tx_tester" - }, - "signature": "70014805da364407f71ca7e09dae5a685af562b985279e370d1a936ebbe7c2545dca35b0f0f30b72eaee0ca042beb6ef2cab5666dd42831a7d0dacac3d962563", - "id": "a50b909f5b483ef1a9bebff66582de169782d3a8adc3e8fca7b183c39738c721" - }, - "serializedHex": "ff011e0100000008000400000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d300f9029500000000001073696d706c655f74785f74657374657270014805da364407f71ca7e09dae5a685af562b985279e370d1a936ebbe7c2545dca35b0f0f30b72eaee0ca042beb6ef2cab5666dd42831a7d0dacac3d962563" -} diff --git a/crypto/fixtures/transactions/username_resignation.json b/crypto/fixtures/transactions/username_resignation.json new file mode 100644 index 0000000..c0a1d77 --- /dev/null +++ b/crypto/fixtures/transactions/username_resignation.json @@ -0,0 +1,18 @@ +{ + "data": { + "nonce": "1", + "gasPrice": "5000000000", + "gasLimit": "200000", + "to": "0x2c1DE3b4Dbb4aDebEbB5dcECAe825bE2a9fc6eb6", + "value": "0", + "data": "ebed6dab", + "network": 11812, + "v": 0, + "r": "9092b27fd1ae599b3248cc0fb3d652b63f77537d0d8d75695394cbb4f2b42d12", + "s": "7d887c8e4b9a989bfb35abe87f6f5850669596b1d4bbc1b42a7a8a04ea982477", + "senderPublicKey": "0243333347c8cbf4e3cbc7a96964181d02a2b0c854faa2fef86b4b8d92afcf473d", + "from": "0x1E6747BEAa5B4076a6A98D735DF8c35a70D18Bdd", + "hash": "204c05f0590157fd71377130ddc962928a53c5b178302061969d294a43ff6909" + }, + "serialized": "f86b0185012a05f20083030d40942c1de3b4dbb4adebebb5dcecae825be2a9fc6eb68084ebed6dab825c6ba09092b27fd1ae599b3248cc0fb3d652b63f77537d0d8d75695394cbb4f2b42d12a07d887c8e4b9a989bfb35abe87f6f5850669596b1d4bbc1b42a7a8a04ea982477" +} \ No newline at end of file diff --git a/crypto/fixtures/transactions/username_resignation/username-resignation-multi-sign.json b/crypto/fixtures/transactions/username_resignation/username-resignation-multi-sign.json deleted file mode 100644 index 94acc2b..0000000 --- a/crypto/fixtures/transactions/username_resignation/username-resignation-multi-sign.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "transaction": { - "version": 1, - "network": 30, - "typeGroup": 1, - "type": 9, - "nonce": "6", - "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", - "fee": "2500000000", - "amount": "0", - "signature": "730f5d46f0ee978f5437899cea0912a87d9854f384a94f3b5b88fbdd527573bb6c6ed13b3da0a695e41efe2b14bafd587db4563b8f29e10f78736959250e8414", - "signatures": [ - "00fe3edd3082e95f42177e6f786f22e3df982e1b85446f9291b6effcdbfe311ec7b9d4df7e68a36ddb6afe04564586c9a3c4245d47077bda91c1fda53d294d7317", - "014e0ca11609da9444233dd13a3561c4ac6f779a177389b498749d70076126b1081d6bab0106938b2431c04c8b4cecb78f8d348ba81e7a03eee5f9fc9b0f1a3131", - "023a7826b28719baf87f4a3a5a10b51d2ce26172a01d92d41691edf7630fd5bd51d9ace6fdfa1361c7a0834d6a6017c3fba4f642fcb2a49e251fbcf6464b5256cc" - ], - "id": "d0210c375d32eec25ca9513099b98e559c3acb136d08011874149217e17ff8f1" - }, - "serializedHex": "ff011e0100000009000600000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d300f902950000000000730f5d46f0ee978f5437899cea0912a87d9854f384a94f3b5b88fbdd527573bb6c6ed13b3da0a695e41efe2b14bafd587db4563b8f29e10f78736959250e841400fe3edd3082e95f42177e6f786f22e3df982e1b85446f9291b6effcdbfe311ec7b9d4df7e68a36ddb6afe04564586c9a3c4245d47077bda91c1fda53d294d7317014e0ca11609da9444233dd13a3561c4ac6f779a177389b498749d70076126b1081d6bab0106938b2431c04c8b4cecb78f8d348ba81e7a03eee5f9fc9b0f1a3131023a7826b28719baf87f4a3a5a10b51d2ce26172a01d92d41691edf7630fd5bd51d9ace6fdfa1361c7a0834d6a6017c3fba4f642fcb2a49e251fbcf6464b5256cc" -} diff --git a/crypto/fixtures/transactions/username_resignation/username-resignation-sign.json b/crypto/fixtures/transactions/username_resignation/username-resignation-sign.json deleted file mode 100644 index 410e21e..0000000 --- a/crypto/fixtures/transactions/username_resignation/username-resignation-sign.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "transaction": { - "version": 1, - "network": 30, - "typeGroup": 1, - "type": 9, - "nonce": "4", - "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", - "fee": "2500000000", - "amount": "0", - "signature": "72b0322b72019f688fc21f367d68b38828c6e3966e52b3fd46f0813cc7c2cdfd58097c4eef8b52896aba1300098e20a38659529b45db9dfca7aa23d560457769", - "id": "1f90e26793dd55b6b2464002dc537882df94199249ba0bd07fcb4512851ed22b" - }, - "serializedHex": "ff011e0100000009000400000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d300f90295000000000072b0322b72019f688fc21f367d68b38828c6e3966e52b3fd46f0813cc7c2cdfd58097c4eef8b52896aba1300098e20a38659529b45db9dfca7aa23d560457769" -} diff --git a/crypto/fixtures/transactions/validator_registration.json b/crypto/fixtures/transactions/validator_registration.json new file mode 100644 index 0000000..81ff420 --- /dev/null +++ b/crypto/fixtures/transactions/validator_registration.json @@ -0,0 +1,18 @@ +{ + "data": { + "nonce": "1", + "gasPrice": "5000000000", + "gasLimit": "200000", + "to": "0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1", + "value": "0", + "data": "226f2645000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000030a18dba7811b212bbb2f080d7c69935998ffbe7b38586e2d3e9e12079ea789996d1c69feb158c002aed327f69865be496000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060a124539f9d469919eb57224cc003d9d5b086a27c6de244abf60b74b35fb749fceab7f4c24b983475cddab7d0876de49c000b5c362f5e3ce18d964f5c2d20d4eadcf7cb77a73d8ee4cd87bad10f7ba0824cea6715d1c045b4f93865a2758b7bfe", + "network": 11812, + "v": 1, + "r": "2ac3550cae0c7749e131b482ea3c5a5d4ca66f4b84e54861ab86cd68340f50a1", + "s": "17101f0c0b91c713db6f79c1aa95c83a2cf7a6c81085692af8526b7b755d5692", + "senderPublicKey": "0243333347c8cbf4e3cbc7a96964181d02a2b0c854faa2fef86b4b8d92afcf473d", + "from": "0x1E6747BEAa5B4076a6A98D735DF8c35a70D18Bdd", + "hash": "cc4331ec856b588436e8ba3de8b04175b4537fbe1cc4dd6a6e8a1f92788b206b" + }, + "serialized": "f9018d0185012a05f20083030d4094535b3d7a252fa034ed71f0c53ec0c6f784cb64e180b90124226f2645000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000030a18dba7811b212bbb2f080d7c69935998ffbe7b38586e2d3e9e12079ea789996d1c69feb158c002aed327f69865be496000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060a124539f9d469919eb57224cc003d9d5b086a27c6de244abf60b74b35fb749fceab7f4c24b983475cddab7d0876de49c000b5c362f5e3ce18d964f5c2d20d4eadcf7cb77a73d8ee4cd87bad10f7ba0824cea6715d1c045b4f93865a2758b7bfe825c6ca02ac3550cae0c7749e131b482ea3c5a5d4ca66f4b84e54861ab86cd68340f50a1a017101f0c0b91c713db6f79c1aa95c83a2cf7a6c81085692af8526b7b755d5692" +} diff --git a/crypto/fixtures/transactions/validator_registration/validator-registration-multi-sign.json b/crypto/fixtures/transactions/validator_registration/validator-registration-multi-sign.json deleted file mode 100644 index 2d6b2cf..0000000 --- a/crypto/fixtures/transactions/validator_registration/validator-registration-multi-sign.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "transaction": { - "version": 1, - "network": 30, - "typeGroup": 1, - "type": 2, - "nonce": "0", - "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", - "fee": "2500000000", - "amount": "0", - "asset": { - "validatorPublicKey": "a08058db53e2665c84a40f5152e76dd2b652125a6079130d4c315e728bcf4dd1dfb44ac26e82302331d61977d3141118" - }, - "signature": "0604c8ecb42e1cc125da7e88fbb6e8b3c3cb703890701b91f8277bf84493ac4994417e8c090309f8147316091d879a1a3c1ce15a6476e029c6f0597a5cfd3533", - "signatures": [ - "00ad621bb19f24c785eded7e3953b575e25fca5b26890eda8ac87cb029d8a28d2c2ef084d98d617fddf7677b27488705e3c5c6d6568a2c44753212e3589e93b89c", - "018de3507e97d8e8e88f77c2cac0c4a7b8767b8527b2a53d81a119e2dbf6672fc8595de1c9ddbb1c88fadf7266923b2289dfbd3b266059c77609bdec52e8efeb61", - "021cbf94035e39cf80b4ccb7ee7d9cfac747ab6e1f73ef4a68b5600ee48c1e94861076718d11ab3908ecc6252c0ea515f6586f966e50842846cce5eada78d84453" - ], - "id": "91c5b9e9dd0915a0e2a44edcae3cd0182ffcdcc3921721fc4a88e5b91a3a1d90" - }, - "serializedHex": "ff011e0100000002000000000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d300f902950000000000a08058db53e2665c84a40f5152e76dd2b652125a6079130d4c315e728bcf4dd1dfb44ac26e82302331d61977d31411180604c8ecb42e1cc125da7e88fbb6e8b3c3cb703890701b91f8277bf84493ac4994417e8c090309f8147316091d879a1a3c1ce15a6476e029c6f0597a5cfd353300ad621bb19f24c785eded7e3953b575e25fca5b26890eda8ac87cb029d8a28d2c2ef084d98d617fddf7677b27488705e3c5c6d6568a2c44753212e3589e93b89c018de3507e97d8e8e88f77c2cac0c4a7b8767b8527b2a53d81a119e2dbf6672fc8595de1c9ddbb1c88fadf7266923b2289dfbd3b266059c77609bdec52e8efeb61021cbf94035e39cf80b4ccb7ee7d9cfac747ab6e1f73ef4a68b5600ee48c1e94861076718d11ab3908ecc6252c0ea515f6586f966e50842846cce5eada78d84453" -} diff --git a/crypto/fixtures/transactions/validator_registration/validator-registration-sign.json b/crypto/fixtures/transactions/validator_registration/validator-registration-sign.json deleted file mode 100644 index ad2fa7b..0000000 --- a/crypto/fixtures/transactions/validator_registration/validator-registration-sign.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "transaction": { - "version": 1, - "network": 30, - "typeGroup": 1, - "type": 2, - "nonce": "0", - "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", - "fee": "2500000000", - "amount": "0", - "asset": { - "validatorPublicKey": "a08058db53e2665c84a40f5152e76dd2b652125a6079130d4c315e728bcf4dd1dfb44ac26e82302331d61977d3141118" - }, - "signature": "449a02672bf54a67ecdbeab20d3fbc16a1358397ebbe28398338f97d9823752f0b3e6c715ce6ad47d8f4df95a7a1ef97b76d159513ba680edecf9e3dd4d76719", - "id": "18659c72ed03091989bf960450ab156d04794ea037357c2f4839d362a1ad576a" - }, - "serializedHex": "ff011e0100000002000000000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d300f902950000000000a08058db53e2665c84a40f5152e76dd2b652125a6079130d4c315e728bcf4dd1dfb44ac26e82302331d61977d3141118449a02672bf54a67ecdbeab20d3fbc16a1358397ebbe28398338f97d9823752f0b3e6c715ce6ad47d8f4df95a7a1ef97b76d159513ba680edecf9e3dd4d76719" -} diff --git a/crypto/fixtures/transactions/validator_resignation.json b/crypto/fixtures/transactions/validator_resignation.json new file mode 100644 index 0000000..2776e42 --- /dev/null +++ b/crypto/fixtures/transactions/validator_resignation.json @@ -0,0 +1,18 @@ +{ + "data": { + "nonce": "1", + "gasPrice": "5000000000", + "gasLimit": "200000", + "to": "0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1", + "value": "0", + "data": "b85f5da2", + "network": 11812, + "v": 1, + "r": "c4cdcf1e6ea401db32e3688aeb2e89e790ce2ea82b57a0125585ba085d6cec5c", + "s": "0a8cde2f42b20a5b6aeb3a4d8443fc5c31f2c3af5eb698551a057bd5709847e3", + "senderPublicKey": "0243333347c8cbf4e3cbc7a96964181d02a2b0c854faa2fef86b4b8d92afcf473d", + "from": "0x1E6747BEAa5B4076a6A98D735DF8c35a70D18Bdd", + "hash": "6bfc80b761bba22759f282a0639637f402e86ca07ca952833e0590592a661401" + }, + "serialized": "f86b0185012a05f20083030d4094535b3d7a252fa034ed71f0c53ec0c6f784cb64e18084b85f5da2825c6ca0c4cdcf1e6ea401db32e3688aeb2e89e790ce2ea82b57a0125585ba085d6cec5ca00a8cde2f42b20a5b6aeb3a4d8443fc5c31f2c3af5eb698551a057bd5709847e3" +} \ No newline at end of file diff --git a/crypto/fixtures/transactions/validator_resignation/validator-resignation-multi-sign.json b/crypto/fixtures/transactions/validator_resignation/validator-resignation-multi-sign.json deleted file mode 100644 index 57fc16d..0000000 --- a/crypto/fixtures/transactions/validator_resignation/validator-resignation-multi-sign.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "transaction": { - "version": 1, - "network": 30, - "typeGroup": 1, - "type": 7, - "nonce": "0", - "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", - "fee": "2500000000", - "amount": "0", - "signature": "a32aeb2cd0695a71f35953e2bac27b36ba51c6fb3f36b98a5e05072500a96206b3afc95efa47d2bbce291b12307c4bd5e79171930e13844a0bc2036516e26644", - "signatures": [ - "00e78bd07e68978b0bcc911c3b4cce17957a347b3ceda0fe78fca3624c6e9d752c46ba75d31004661d097bc6c2471f86fb6727ac9e385c931d2850fc1ad3eb98c0", - "01706c56c0a5568df17342bb0fffec66cbf948425153f31ee3f16760c372ae4e020f2489695c3f3a80524bbba6fa97aef074259e8dc9305966dfc1a40a632872d0", - "021e218f51b7803c327c092f89af015573371506dd8f27a96dacaf29286d8ecc5d955443cfb1a5d93f359ebef67fdb842fc3e9a8d9691e57a5f627cb11425526e0" - ], - "id": "97ff3cf77125fea2ce33f23ef4ce85d56e207bc47e1595c004a319976fdca64c" - }, - "serializedHex": "ff011e0100000007000000000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d300f902950000000000a32aeb2cd0695a71f35953e2bac27b36ba51c6fb3f36b98a5e05072500a96206b3afc95efa47d2bbce291b12307c4bd5e79171930e13844a0bc2036516e2664400e78bd07e68978b0bcc911c3b4cce17957a347b3ceda0fe78fca3624c6e9d752c46ba75d31004661d097bc6c2471f86fb6727ac9e385c931d2850fc1ad3eb98c001706c56c0a5568df17342bb0fffec66cbf948425153f31ee3f16760c372ae4e020f2489695c3f3a80524bbba6fa97aef074259e8dc9305966dfc1a40a632872d0021e218f51b7803c327c092f89af015573371506dd8f27a96dacaf29286d8ecc5d955443cfb1a5d93f359ebef67fdb842fc3e9a8d9691e57a5f627cb11425526e0" -} diff --git a/crypto/fixtures/transactions/validator_resignation/validator-resignation-sign.json b/crypto/fixtures/transactions/validator_resignation/validator-resignation-sign.json deleted file mode 100644 index 6bb87f3..0000000 --- a/crypto/fixtures/transactions/validator_resignation/validator-resignation-sign.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "transaction": { - "version": 1, - "network": 30, - "typeGroup": 1, - "type": 7, - "nonce": "0", - "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", - "fee": "2500000000", - "amount": "0", - "signature": "f9d93dc161b5351a9e6f89e97bd3a4d1b73349c2339145021dee2654f5a8116aa78c6c3742b5fc39d0c9265735b866954d8c6c8383130d08659b1aff3967d4eb", - "id": "431b779764f51e227eca907706bd339de36c3b23bd46c603283f3a932d12b936" - }, - "serializedHex": "ff011e0100000007000000000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d300f902950000000000f9d93dc161b5351a9e6f89e97bd3a4d1b73349c2339145021dee2654f5a8116aa78c6c3742b5fc39d0c9265735b866954d8c6c8383130d08659b1aff3967d4eb" -} diff --git a/crypto/fixtures/transactions/validator_update.json b/crypto/fixtures/transactions/validator_update.json new file mode 100644 index 0000000..0e1c2a3 --- /dev/null +++ b/crypto/fixtures/transactions/validator_update.json @@ -0,0 +1,18 @@ +{ + "data": { + "nonce": "1", + "gasPrice": "5000000000", + "gasLimit": "200000", + "to": "0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1", + "value": "0", + "data": "8f062626000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000030a18dba7811b212bbb2f080d7c69935998ffbe7b38586e2d3e9e12079ea789996d1c69feb158c002aed327f69865be496000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060a124539f9d469919eb57224cc003d9d5b086a27c6de244abf60b74b35fb749fceab7f4c24b983475cddab7d0876de49c000b5c362f5e3ce18d964f5c2d20d4eadcf7cb77a73d8ee4cd87bad10f7ba0824cea6715d1c045b4f93865a2758b7bfe", + "network": 11812, + "v": 0, + "r": "5500089b0b0afbf2a638d7fab985d8def9e44c0087859f6e5e0561e37dccc487", + "s": "219b047c62e0d94b3e834085dc0cd6b5f12c6f27ab058e949dec82f24f29b4fd", + "senderPublicKey": "0243333347c8cbf4e3cbc7a96964181d02a2b0c854faa2fef86b4b8d92afcf473d", + "from": "0x1E6747BEAa5B4076a6A98D735DF8c35a70D18Bdd", + "hash": "142c471a038d69b7ae7d18c83efa767a16915dc750a380966f50959beec9f9ea" + }, + "serialized": "f9018d0185012a05f20083030d4094535b3d7a252fa034ed71f0c53ec0c6f784cb64e180b901248f062626000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000030a18dba7811b212bbb2f080d7c69935998ffbe7b38586e2d3e9e12079ea789996d1c69feb158c002aed327f69865be496000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060a124539f9d469919eb57224cc003d9d5b086a27c6de244abf60b74b35fb749fceab7f4c24b983475cddab7d0876de49c000b5c362f5e3ce18d964f5c2d20d4eadcf7cb77a73d8ee4cd87bad10f7ba0824cea6715d1c045b4f93865a2758b7bfe825c6ba05500089b0b0afbf2a638d7fab985d8def9e44c0087859f6e5e0561e37dccc487a0219b047c62e0d94b3e834085dc0cd6b5f12c6f27ab058e949dec82f24f29b4fd" +} diff --git a/crypto/fixtures/transactions/vote.json b/crypto/fixtures/transactions/vote.json new file mode 100644 index 0000000..f412de2 --- /dev/null +++ b/crypto/fixtures/transactions/vote.json @@ -0,0 +1,18 @@ +{ + "data": { + "nonce": "1", + "gasPrice": "5000000000", + "gasLimit": "200000", + "to": "0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1", + "value": "0", + "data": "6dd7d8ea000000000000000000000000c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763", + "network": 11812, + "v": 1, + "r": "48cdb8cd112e05823e227319b66f2ef5e89c16ff5c568edab1cfa5f3fd8401c0", + "s": "264a4bcd27a62696e15e8588765b2739c3ed34f4b83aacd87998b2ac4c4335ec", + "senderPublicKey": "0243333347c8cbf4e3cbc7a96964181d02a2b0c854faa2fef86b4b8d92afcf473d", + "from": "0x1E6747BEAa5B4076a6A98D735DF8c35a70D18Bdd", + "hash": "7885cf77a0b272488efa03ed14994f2560490d14bd5df9b5fbdb7a6f389c8713" + }, + "serialized": "f88b0185012a05f20083030d4094535b3d7a252fa034ed71f0c53ec0c6f784cb64e180a46dd7d8ea000000000000000000000000c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763825c6ca048cdb8cd112e05823e227319b66f2ef5e89c16ff5c568edab1cfa5f3fd8401c0a0264a4bcd27a62696e15e8588765b2739c3ed34f4b83aacd87998b2ac4c4335ec" +} \ No newline at end of file diff --git a/crypto/fixtures/transactions/vote/vote-multi-sign.json b/crypto/fixtures/transactions/vote/vote-multi-sign.json deleted file mode 100644 index 64305a8..0000000 --- a/crypto/fixtures/transactions/vote/vote-multi-sign.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "transaction": { - "version": 1, - "network": 30, - "typeGroup": 1, - "type": 3, - "nonce": "1", - "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", - "fee": "100000000", - "amount": "0", - "asset": { - "votes": [ - "03f25455408f9a7e6c6a056b121e68fbda98f3511d22e9ef27b0ebaf1ef9e4eabc" - ] - }, - "signature": "72a097e05983575a69ab7c2c15d80beea988fc02e08124a969b281fa09d1e47ff0b36706f9a9bb6b2e18db929f21b08571c4be36fe2882fecfb990e465bfebde", - "signatures": [ - "0035173f507953f5a7f9da525384b9a1021b553c1089f060e6e77118ab5aa68549ea89d4b63391941e95c0142dd195fd8c032c0a9aca9e1a72943dde8ecd663070", - "010dba0e40f903c7ec76e1f1e7ef3c145e69a878b13e833d132b9f1fa267ba3b2ab049941ede13faee3da9611ac27cb52a6888bbd093a253e7d7bea4c633d4a706", - "02556654e210ed6426d20c0fe246d3270bcdea97d5ab59f5c90888731b079ff83ab53cac3bc80680f093f2e5a78216a0ed894c6985bd89b3802b2d8ff3f6b4c584" - ], - "id": "e344602ba9fe82d035af6c8b759682bab0317ca523212724542217803c093adb" - }, - "serializedHex": "ff011e0100000003000100000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d300e1f50500000000000103f25455408f9a7e6c6a056b121e68fbda98f3511d22e9ef27b0ebaf1ef9e4eabc0072a097e05983575a69ab7c2c15d80beea988fc02e08124a969b281fa09d1e47ff0b36706f9a9bb6b2e18db929f21b08571c4be36fe2882fecfb990e465bfebde0035173f507953f5a7f9da525384b9a1021b553c1089f060e6e77118ab5aa68549ea89d4b63391941e95c0142dd195fd8c032c0a9aca9e1a72943dde8ecd663070010dba0e40f903c7ec76e1f1e7ef3c145e69a878b13e833d132b9f1fa267ba3b2ab049941ede13faee3da9611ac27cb52a6888bbd093a253e7d7bea4c633d4a70602556654e210ed6426d20c0fe246d3270bcdea97d5ab59f5c90888731b079ff83ab53cac3bc80680f093f2e5a78216a0ed894c6985bd89b3802b2d8ff3f6b4c584" -} diff --git a/crypto/fixtures/transactions/vote/vote-sign.json b/crypto/fixtures/transactions/vote/vote-sign.json deleted file mode 100644 index b1d260e..0000000 --- a/crypto/fixtures/transactions/vote/vote-sign.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "transaction": { - "version": 1, - "network": 30, - "typeGroup": 1, - "type": 3, - "nonce": "1", - "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", - "fee": "100000000", - "amount": "0", - "asset": { - "votes": [ - "03f25455408f9a7e6c6a056b121e68fbda98f3511d22e9ef27b0ebaf1ef9e4eabc" - ] - }, - "signature": "e0a99c2ebd108c6ba1828f4d11e5ce8cdf2e67152575cabea2e86ab59153a1f1493b768af6dfe0a33eec22edf2dd87c25e1531bf285bb4131e4fcdbfce4c8781", - "id": "81bffacc80ae1c3b3f127cbaf5d2867d3209c38610d2cc5a341702fc252cd330" - }, - "serializedHex": "ff011e0100000003000100000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d300e1f50500000000000103f25455408f9a7e6c6a056b121e68fbda98f3511d22e9ef27b0ebaf1ef9e4eabc00e0a99c2ebd108c6ba1828f4d11e5ce8cdf2e67152575cabea2e86ab59153a1f1493b768af6dfe0a33eec22edf2dd87c25e1531bf285bb4131e4fcdbfce4c8781" -} diff --git a/crypto/serdeser_test.go b/crypto/serdeser_test.go new file mode 100644 index 0000000..6dc79b5 --- /dev/null +++ b/crypto/serdeser_test.go @@ -0,0 +1,200 @@ +package crypto + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const evmCallFixturePassphrase = "found lobster oblige describe ready addict body brave live vacuum display salute lizard combine gift resemble race senior quality reunion proud tell adjust angle" + +func mustBigInt(t *testing.T, s string) *big.Int { + t.Helper() + n, ok := new(big.Int).SetString(s, 10) + require.New(t).True(ok, "not a valid base-10 integer: %q", s) + return n +} + +func transactionFromFixture(t *testing.T, fixture TestingTransactionFixture) *Transaction { + t.Helper() + return &Transaction{ + Nonce: mustBigInt(t, fixture.Data.Nonce), + GasPrice: mustBigInt(t, fixture.Data.GasPrice), + GasLimit: mustBigInt(t, fixture.Data.GasLimit), + To: fixture.Data.To, + Value: mustBigInt(t, fixture.Data.Value), + Data: HexDecode(fixture.Data.Data), + } +} + +func TestFixturesMatchPhpTsSerialization(t *testing.T) { + names := []string{ + "transfer", + "vote", + "unvote", + "validator_registration", + "validator_update", + "validator_resignation", + "username_registration", + "username_resignation", + "multi_payment", + } + + for _, name := range names { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + fixture := GetTransactionFixture(name) + tx := transactionFromFixture(t, fixture) + + require.NoError(tx.Sign(evmCallFixturePassphrase)) + + assert.Equal(fixture.Data.V, tx.V) + assert.Equal(fixture.Data.R, HexEncode(tx.R)) + assert.Equal(fixture.Data.S, HexEncode(tx.S)) + assert.Equal(fixture.Data.SenderPublicKey, tx.SenderPublicKey) + assert.Equal(fixture.Data.From, tx.From) + assert.Equal(fixture.Data.Hash, tx.Hash) + assert.Equal(fixture.Serialized, HexEncode(tx.Serialized)) + + verified, err := tx.Verify() + require.NoError(err) + assert.True(verified) + + deserialized, err := DeserializeTransaction(fixture.Serialized) + require.NoError(err) + + deserializedVerified, err := deserialized.Verify() + require.NoError(err) + assert.True(deserializedVerified) + + assert.Equal(tx.Hash, deserialized.Hash) + assert.Equal(tx.From, deserialized.From) + assert.Equal(tx.SenderPublicKey, deserialized.SenderPublicKey) + assert.Equal(tx.To, deserialized.To) + assert.Equal(0, tx.Value.Cmp(deserialized.Value)) + assert.Equal(tx.Data, deserialized.Data) + }) + } +} + +func TestFixtureSemanticFieldsDecodeCorrectly(t *testing.T) { + t.Run("vote", func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + fixture := GetTransactionFixture("vote") + deserialized, err := DeserializeTransaction(fixture.Serialized) + require.NoError(err) + + validatorBytes := HexDecode("c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763") + + assert.True(IsVote(deserialized.Data)) + assert.Equal(AddressFromBytes(validatorBytes), deserialized.Vote) + }) + + t.Run("unvote", func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + fixture := GetTransactionFixture("unvote") + deserialized, err := DeserializeTransaction(fixture.Serialized) + require.NoError(err) + + assert.True(IsUnvote(deserialized.Data)) + }) + + t.Run("validator_registration", func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + fixture := GetTransactionFixture("validator_registration") + deserialized, err := DeserializeTransaction(fixture.Serialized) + require.NoError(err) + + assert.True(IsValidatorRegistration(deserialized.Data)) + assert.False(IsUpdateValidator(deserialized.Data)) + assert.Len(deserialized.ValidatorPublicKey, 96) // 48-byte BLS G1 pubkey, hex-encoded + assert.Len(deserialized.ValidatorProof, 192) // 96-byte BLS G2 PoP signature, hex-encoded + }) + + t.Run("validator_update", func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + fixture := GetTransactionFixture("validator_update") + deserialized, err := DeserializeTransaction(fixture.Serialized) + require.NoError(err) + + assert.True(IsUpdateValidator(deserialized.Data)) + assert.False(IsValidatorRegistration(deserialized.Data)) + assert.Len(deserialized.ValidatorPublicKey, 96) + assert.Len(deserialized.ValidatorProof, 192) + }) + + t.Run("validator_resignation", func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + fixture := GetTransactionFixture("validator_resignation") + deserialized, err := DeserializeTransaction(fixture.Serialized) + require.NoError(err) + + assert.True(IsValidatorResignation(deserialized.Data)) + }) + + t.Run("username_registration", func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + fixture := GetTransactionFixture("username_registration") + deserialized, err := DeserializeTransaction(fixture.Serialized) + require.NoError(err) + + assert.True(IsUsernameRegistration(deserialized.Data)) + assert.Equal("fixture", deserialized.Username) + }) + + t.Run("username_resignation", func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + fixture := GetTransactionFixture("username_resignation") + deserialized, err := DeserializeTransaction(fixture.Serialized) + require.NoError(err) + + assert.True(IsUsernameResignation(deserialized.Data)) + }) + + t.Run("multi_payment", func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + fixture := GetTransactionFixture("multi_payment") + deserialized, err := DeserializeTransaction(fixture.Serialized) + require.NoError(err) + + addr1 := HexDecode("6f0182a0cc707b055322ccf6d4cb6a5aff1aeb22") + addr2 := HexDecode("c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763") + + assert.True(IsMultiPayment(deserialized.Data)) + assert.Equal([]string{AddressFromBytes(addr1), AddressFromBytes(addr2)}, deserialized.PaymentAddresses) + require.Len(deserialized.PaymentAmounts, 2) + assert.Equal(0, big.NewInt(100000).Cmp(deserialized.PaymentAmounts[0])) + assert.Equal(0, big.NewInt(200000).Cmp(deserialized.PaymentAmounts[1])) + }) + + t.Run("transfer", func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + fixture := GetTransactionFixture("transfer") + deserialized, err := DeserializeTransaction(fixture.Serialized) + require.NoError(err) + + assert.True(IsTransfer(deserialized.Data)) + }) +}