diff --git a/qkc/types/cross_shard_transaction.go b/qkc/types/cross_shard_transaction.go new file mode 100644 index 000000000000..6486186b84a1 --- /dev/null +++ b/qkc/types/cross_shard_transaction.go @@ -0,0 +1,79 @@ +// Copyright 2026-2027, QuarkChain. + +// Cross-shard transactions follow pyquarkchain-compatible QKC wire encoding. + +package types + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/qkc/account" + "github.com/ethereum/go-ethereum/qkc/serialize" +) + +const crossShardTransactionListVersion = uint32(1) + +// CrossShardTransactionDeposit matches pyquarkchain's current +// CrossShardTransactionDeposit.FIELDS order. +type CrossShardTransactionDeposit struct { + TxHash common.Hash + From account.Address + To account.Address + Value *serialize.Uint256 + GasPrice *serialize.Uint256 + GasTokenID uint64 + TransferTokenID uint64 + GasRemained *serialize.Uint256 + MessageData []byte `bytesizeofslicelen:"4"` + CreateContract bool + IsFromRootChain bool + RefundRate uint8 +} + +// CrossShardTransactionList is pyquarkchain's CrossShardTransactionList +// version 1 wire type. +type CrossShardTransactionList struct { + TXList []*CrossShardTransactionDeposit +} + +func NewCrossShardTransactionList(txList []*CrossShardTransactionDeposit) *CrossShardTransactionList { + if txList == nil { + txList = make([]*CrossShardTransactionDeposit, 0) + } + return &CrossShardTransactionList{ + TXList: txList, + } +} + +// Serialize writes pyquarkchain CrossShardTransactionList.FIELDS order: +// tx_list followed by version(uint32). +func (c *CrossShardTransactionList) Serialize(w *[]byte) error { + if c == nil { + return fmt.Errorf("nil cross-shard transaction list") + } + if err := serialize.SerializeWithTags(w, c.TXList, serialize.Tags{ByteSizeOfSliceLen: 4}); err != nil { + return err + } + return serialize.Serialize(w, crossShardTransactionListVersion) +} + +// Deserialize reads the current pyquarkchain CrossShardTransactionList version. +func (c *CrossShardTransactionList) Deserialize(bb *serialize.ByteBuffer) error { + if c == nil { + return fmt.Errorf("nil cross-shard transaction list") + } + var txList []*CrossShardTransactionDeposit + if err := serialize.DeserializeWithTags(bb, &txList, serialize.Tags{ByteSizeOfSliceLen: 4}); err != nil { + return err + } + var version uint32 + if err := serialize.Deserialize(bb, &version); err != nil { + return err + } + if version != crossShardTransactionListVersion { + return fmt.Errorf("unsupported cross-shard transaction list version %d", version) + } + c.TXList = txList + return nil +} diff --git a/qkc/types/cross_shard_transaction_test.go b/qkc/types/cross_shard_transaction_test.go new file mode 100644 index 000000000000..ee3ff7850761 --- /dev/null +++ b/qkc/types/cross_shard_transaction_test.go @@ -0,0 +1,106 @@ +// Copyright 2026-2027, QuarkChain. + +// Cross-shard transaction tests exercise pyquarkchain-compatible QKC wire bytes. + +package types + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/qkc/account" + "github.com/ethereum/go-ethereum/qkc/serialize" + "github.com/stretchr/testify/assert" +) + +func TestCrossShardTransactionList(t *testing.T) { + c1 := NewCrossShardTransactionList(nil) + for index := uint64(0); index < 100; index++ { + u256 := new(serialize.Uint256) + u256.Value = new(big.Int).SetUint64(index) + c1.TXList = append(c1.TXList, &CrossShardTransactionDeposit{ + TxHash: common.BigToHash(new(big.Int).SetUint64(index)), + From: account.Address{ + Recipient: common.BigToAddress(new(big.Int).SetUint64(2)), + FullShardKey: 2, + }, + To: account.Address{ + Recipient: common.BigToAddress(new(big.Int).SetUint64(3)), + FullShardKey: 3, + }, + Value: u256, + GasPrice: u256, + GasTokenID: 123, + TransferTokenID: 456, + IsFromRootChain: false, + GasRemained: u256, + MessageData: []byte{}, + CreateContract: true, + RefundRate: uint8(index), + }) + } + + data, err := serialize.SerializeToBytes(c1) + assert.NoError(t, err) + + d1 := NewCrossShardTransactionList(nil) + err = serialize.DeserializeFromBytes(data, d1) + assert.NoError(t, err) + for k, v := range c1.TXList { + assert.Equal(t, v.TxHash, (*d1).TXList[k].TxHash) + assert.Equal(t, v.From, (*d1).TXList[k].From) + assert.Equal(t, v.To, (*d1).TXList[k].To) + assert.Equal(t, v.Value.Value.Uint64(), (*d1).TXList[k].Value.Value.Uint64()) + assert.Equal(t, v.GasPrice.Value.Uint64(), (*d1).TXList[k].GasPrice.Value.Uint64()) + assert.Equal(t, v.GasTokenID, (*d1).TXList[k].GasTokenID) + assert.Equal(t, v.TransferTokenID, (*d1).TXList[k].TransferTokenID) + assert.Equal(t, v.IsFromRootChain, (*d1).TXList[k].IsFromRootChain) + assert.Equal(t, v.GasRemained.Value.Uint64(), (*d1).TXList[k].GasRemained.Value.Uint64()) + assert.Equal(t, v.MessageData, (*d1).TXList[k].MessageData) + assert.Equal(t, v.CreateContract, (*d1).TXList[k].CreateContract) + assert.Equal(t, uint8(k), (*d1).TXList[k].RefundRate) + } + +} + +func TestCrossShardTransactionListPyquarkchainGolden(t *testing.T) { + assertSerialized := func(t *testing.T, list *CrossShardTransactionList, expected string) { + t.Helper() + encoded, err := serialize.SerializeToBytes(list) + assert.NoError(t, err) + assert.Equal(t, expected, common.Bytes2Hex(encoded)) + + var decoded CrossShardTransactionList + err = serialize.DeserializeFromBytes(common.FromHex(expected), &decoded) + assert.NoError(t, err) + reencoded, err := serialize.SerializeToBytes(&decoded) + assert.NoError(t, err) + assert.Equal(t, expected, common.Bytes2Hex(reencoded)) + } + + // Generated by pyquarkchain's CrossShardTransactionList.serialize(). + assertSerialized(t, NewCrossShardTransactionList(nil), "0000000000000001") + + u256 := func(value int64) *serialize.Uint256 { return &serialize.Uint256{Value: big.NewInt(value)} } + assertSerialized(t, NewCrossShardTransactionList([]*CrossShardTransactionDeposit{{ + TxHash: common.HexToHash("0x1111111111111111111111111111111111111111111111111111111111111111"), + From: account.Address{Recipient: common.HexToAddress("0x2222222222222222222222222222222222222222"), FullShardKey: 3}, + To: account.Address{Recipient: common.HexToAddress("0x3333333333333333333333333333333333333333"), FullShardKey: 4}, + Value: u256(5), + GasPrice: u256(6), + GasTokenID: 7, + TransferTokenID: 8, + GasRemained: u256(9), + MessageData: []byte{0xaa, 0xbb}, + CreateContract: true, + IsFromRootChain: true, + RefundRate: 10, + }}), "0000000111111111111111111111111111111111111111111111111111111111111111112222222222222222222222222222222222222222000000033333333333333333333333333333333333333333000000040000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000070000000000000008000000000000000000000000000000000000000000000000000000000000000900000002aabb01010a00000001") +} + +func TestCrossShardTransactionListRejectsUnsupportedVersion(t *testing.T) { + var list CrossShardTransactionList + err := serialize.DeserializeFromBytes(common.FromHex("0000000000000000"), &list) + assert.Error(t, err) +} diff --git a/qkc/types/transaction.go b/qkc/types/transaction.go new file mode 100644 index 000000000000..b7b91086c276 --- /dev/null +++ b/qkc/types/transaction.go @@ -0,0 +1,483 @@ +// Copyright 2026-2027, QuarkChain. + +// Transactions follow pyquarkchain-compatible QKC wire encoding. +// Modified from go-ethereum under GNU Lesser General Public License +// Adaptation: sha3.NewKeccak256() -> crypto.NewKeccakState() (identical Keccak-256 digest). + +package types + +import ( + "errors" + "fmt" + "io" + "math/big" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/qkc/account" + qkcCommon "github.com/ethereum/go-ethereum/qkc/common" + "github.com/ethereum/go-ethereum/qkc/serialize" + "github.com/ethereum/go-ethereum/rlp" +) + +const ( + EvmTx = 0 +) + +//go:generate gencodec -type txdata -field-override txdataMarshaling -out gen_tx_json.go + +var ( + ErrInvalidSig = errors.New("invalid transaction v, r, s values") +) + +type EvmTransaction struct { + data txdata + // caches + hashDirty bool // true after a setter changes the inner RLP payload. + hash atomic.Value // RLP hash of the inner EVM transaction. + size atomic.Value + from atomic.Value + FromShardsize uint32 + ToShardsize uint32 +} + +type txdata struct { + AccountNonce uint64 `json:"nonce" gencodec:"required"` + Price *big.Int `json:"gasPrice" gencodec:"required"` + GasLimit uint64 `json:"gas" gencodec:"required"` + Recipient *account.Recipient `json:"to" rlp:"nil"` // nil means contract creation + Amount *big.Int `json:"value" gencodec:"required"` + Payload []byte `json:"input" gencodec:"required"` + NetworkId uint32 `json:"networkId" gencodec:"required"` + FromFullShardKey *Uint32 `json:"fromfullshardkey" gencodec:"required"` + ToFullShardKey *Uint32 `json:"tofullshardkey" gencodec:"required"` + GasTokenID uint64 `json:"gas_token_id" gencodec:"required"` + TransferTokenID uint64 `json:"transfer_token_id" gencodec:"required"` + Version uint32 `json:"version" gencodec:"required"` + // Signature values + V *big.Int `json:"v" gencodec:"required"` + R *big.Int `json:"r" gencodec:"required"` + S *big.Int `json:"s" gencodec:"required"` +} + +func NewEvmTransaction(nonce uint64, to account.Recipient, amount *big.Int, gasLimit uint64, gasPrice *big.Int, fromFullShardKey uint32, + toFullShardKey uint32, networkId uint32, version uint32, data []byte, gasTokenID, transferTokenID uint64) *EvmTransaction { + return newEvmTransaction(nonce, &to, amount, gasLimit, gasPrice, fromFullShardKey, toFullShardKey, networkId, version, data, gasTokenID, transferTokenID) +} + +func (e *EvmTransaction) SetGas(data uint64) { + e.data.GasLimit = data + e.hashDirty = true +} + +func (e *EvmTransaction) SetFromFullShardKey(data uint32) { + t := Uint32(data) + e.data.FromFullShardKey = &t + e.hashDirty = true +} + +func (e *EvmTransaction) SetNonce(data uint64) { + e.data.AccountNonce = data + e.hashDirty = true +} + +func (e *EvmTransaction) SetVRS(v, r, s *big.Int) { + e.data.V = v + e.data.R = r + e.data.S = s + e.hashDirty = true +} + +func (e *EvmTransaction) SetSender(signer Signer, addr account.Recipient) { + e.from.Store(sigCache{signer: signer, from: addr}) +} + +func NewEvmContractCreation(nonce uint64, amount *big.Int, gasLimit uint64, gasPrice *big.Int, fromFullShardKey uint32, toFullShardKey uint32, networkId uint32, version uint32, data []byte, gasTokenID, transferTokenID uint64) *EvmTransaction { + return newEvmTransaction(nonce, nil, amount, gasLimit, gasPrice, fromFullShardKey, toFullShardKey, networkId, version, data, gasTokenID, transferTokenID) +} + +func newEvmTransaction(nonce uint64, to *account.Recipient, amount *big.Int, gasLimit uint64, gasPrice *big.Int, fromFullShardKey uint32, toFullShardKey uint32, networkId uint32, version uint32, data []byte, gasTokenID, transferTokenID uint64) *EvmTransaction { + newFromFullShardKey := Uint32(fromFullShardKey) + newToFullShardKey := Uint32(toFullShardKey) + if len(data) > 0 { + data = common.CopyBytes(data) + } + d := txdata{ + AccountNonce: nonce, + Recipient: to, + Payload: data, + Amount: new(big.Int), + GasLimit: gasLimit, + Price: new(big.Int), + FromFullShardKey: &newFromFullShardKey, + ToFullShardKey: &newToFullShardKey, + GasTokenID: gasTokenID, + TransferTokenID: transferTokenID, + NetworkId: networkId, + Version: version, + V: new(big.Int), + R: new(big.Int), + S: new(big.Int), + } + if amount != nil { + d.Amount.Set(amount) + } + if gasPrice != nil { + d.Price.Set(gasPrice) + } + + return &EvmTransaction{data: d} +} + +// EncodeRLP implements rlp.Encoder +func (tx *EvmTransaction) EncodeRLP(w io.Writer) error { + return rlp.Encode(w, &tx.data) +} + +// DecodeRLP implements rlp.Decoder +func (tx *EvmTransaction) DecodeRLP(s *rlp.Stream) error { + _, size, _ := s.Kind() + err := s.Decode(&tx.data) + if err == nil { + tx.size.Store(common.StorageSize(rlp.ListSize(size))) + } + + return err +} + +type txdataUnsigned struct { + AccountNonce uint64 `json:"nonce" gencodec:"required"` + Price *big.Int `json:"gasPrice" gencodec:"required"` + GasLimit uint64 `json:"gas" gencodec:"required"` + Recipient *account.Recipient `json:"to" rlp:"nil"` // nil means contract creation + Amount *big.Int `json:"value" gencodec:"required"` + Payload []byte `json:"input" gencodec:"required"` + NetworkId uint32 `json:"networkid" gencodec:"required"` + FromFullShardKey *Uint32 `json:"fromfullshardid" gencodec:"required"` + ToFullShardKey *Uint32 `json:"tofullshardid" gencodec:"required"` + GasTokenID uint64 `json:"gasTokenID" gencodec:"required"` + TransferTokenID uint64 `json:"transferTokenID" gencodec:"required"` +} + +func (tx *EvmTransaction) getUnsignedHash() common.Hash { + unsigntx := txdataUnsigned{ + AccountNonce: tx.data.AccountNonce, + Price: tx.data.Price, + GasLimit: tx.data.GasLimit, + Recipient: tx.data.Recipient, + Amount: tx.data.Amount, + Payload: tx.data.Payload, + FromFullShardKey: tx.data.FromFullShardKey, + ToFullShardKey: tx.data.ToFullShardKey, + GasTokenID: tx.data.GasTokenID, + TransferTokenID: tx.data.TransferTokenID, + NetworkId: tx.data.NetworkId, + } + return rlpHash(unsigntx) +} + +func (tx *EvmTransaction) getUnsignedHashForEip155(chainId uint32) common.Hash { + return rlpHash([]interface{}{ + tx.data.AccountNonce, + tx.data.Price, + tx.data.GasLimit, + tx.data.Recipient, + tx.data.Amount, + tx.data.Payload, + chainId, uint(0), uint(0), + }) +} + +func (tx *EvmTransaction) typedHash() (common.Hash, error) { + sigHash, err := typedSignatureHash(evmTxToTypedData(tx)) + if err != nil { + return common.Hash{}, err + } + bytes := common.FromHex(sigHash) + return common.BytesToHash(bytes), nil +} + +func (tx *EvmTransaction) Data() []byte { return common.CopyBytes(tx.data.Payload) } +func (tx *EvmTransaction) Gas() uint64 { return tx.data.GasLimit } +func (tx *EvmTransaction) GasPrice() *big.Int { return new(big.Int).Set(tx.data.Price) } +func (tx *EvmTransaction) Value() *big.Int { return new(big.Int).Set(tx.data.Amount) } +func (tx *EvmTransaction) Nonce() uint64 { return tx.data.AccountNonce } +func (tx *EvmTransaction) FromFullShardId() uint32 { + return tx.FromChainID()<<16 | tx.FromShardSize() | tx.FromShardID() +} +func (tx *EvmTransaction) ToFullShardId() uint32 { + return tx.ToChainID()<<16 | tx.ToShardSize() | tx.ToShardID() +} +func (tx *EvmTransaction) NetworkId() uint32 { return tx.data.NetworkId } +func (tx *EvmTransaction) Version() uint32 { return tx.data.Version } +func (tx *EvmTransaction) IsCrossShard() bool { + return !(tx.FromChainID() == tx.ToChainID() && tx.FromShardID() == tx.ToShardID()) +} +func (tx *EvmTransaction) GasTokenID() uint64 { + return tx.data.GasTokenID +} +func (tx *EvmTransaction) TransferTokenID() uint64 { + return tx.data.TransferTokenID +} +func (tx *EvmTransaction) FromFullShardKey() uint32 { return tx.data.FromFullShardKey.GetValue() } +func (tx *EvmTransaction) ToFullShardKey() uint32 { return tx.data.ToFullShardKey.GetValue() } +func (tx *EvmTransaction) FromChainID() uint32 { return tx.data.FromFullShardKey.GetValue() >> 16 } +func (tx *EvmTransaction) ToChainID() uint32 { return tx.data.ToFullShardKey.GetValue() >> 16 } +func (tx *EvmTransaction) FromShardSize() uint32 { + return tx.FromShardsize +} +func (tx *EvmTransaction) ToShardSize() uint32 { + return tx.ToShardsize +} +func (tx *EvmTransaction) SetFromShardSize(shardSize uint32) error { + if !qkcCommon.IsP2(shardSize) || shardSize == 0 { + return errors.New("shardSize is not Usable") + } + tx.FromShardsize = shardSize + return nil +} +func (tx *EvmTransaction) SetToShardSize(shardSize uint32) error { + if !qkcCommon.IsP2(shardSize) || shardSize == 0 { + return errors.New("shardSize is not Usable") + } + tx.ToShardsize = shardSize + return nil +} + +func (tx *EvmTransaction) FromShardKey() uint32 { + shardMask := uint32(65535) + return tx.data.FromFullShardKey.GetValue() & shardMask +} + +func (tx *EvmTransaction) ToShardKey() uint32 { + shardMask := uint32(65535) + return tx.data.ToFullShardKey.GetValue() & shardMask +} + +func (tx *EvmTransaction) FromShardID() uint32 { + shardMask := tx.FromShardSize() - 1 + return tx.data.FromFullShardKey.GetValue() & shardMask +} +func (tx *EvmTransaction) ToShardID() uint32 { + shardMask := tx.ToShardSize() - 1 + return tx.data.ToFullShardKey.GetValue() & shardMask +} + +// To returns the recipient address of the transaction. +// It returns nil if the transaction is a contract creation. +func (tx *EvmTransaction) To() *account.Recipient { + if tx.data.Recipient == nil { + return nil + } + + to := *tx.data.Recipient + return &to +} + +// Hash hashes the RLP encoding of tx. +// It uniquely identifies the transaction. +func (tx *EvmTransaction) Hash() common.Hash { + if hash := tx.hash.Load(); hash != nil && !tx.hashDirty { + return hash.(common.Hash) + } + v := rlpHash(tx) + tx.hash.Store(v) + tx.hashDirty = false + return v +} + +// Size returns the true RLP encoded storage size of the transaction, either by +// encoding and returning it, or returning a previsouly cached value. +func (tx *EvmTransaction) Size() common.StorageSize { + if size := tx.size.Load(); size != nil { + return size.(common.StorageSize) + } + c := writeCounter(0) + rlp.Encode(&c, &tx.data) + tx.size.Store(common.StorageSize(c)) + return common.StorageSize(c) +} + +// WithSignature returns a new transaction with the given signature. +// This signature needs to be formatted as described in the yellow paper (v+27). +func (tx *EvmTransaction) WithSignature(signer Signer, sig []byte) (*EvmTransaction, error) { + r, s, v, err := signer.SignatureValues(tx, sig) + if err != nil { + return nil, err + } + cpy := &EvmTransaction{data: tx.data} + cpy.data.R, cpy.data.S, cpy.data.V = r, s, v + return cpy, nil +} + +// Cost returns amount + gasprice * gaslimit. +func (tx *EvmTransaction) Cost() *big.Int { + total := new(big.Int).Mul(tx.data.Price, new(big.Int).SetUint64(tx.data.GasLimit)) + total.Add(total, tx.data.Amount) + return total +} + +func (tx *EvmTransaction) RawSignatureValues() (*big.Int, *big.Int, *big.Int) { + return tx.data.V, tx.data.R, tx.data.S +} + +func rlpHash(x interface{}) (h common.Hash) { + hw := crypto.NewKeccakState() + rlp.Encode(hw, x) + hw.Sum(h[:0]) + return h +} + +type Transaction struct { + TxType uint8 + EvmTx *EvmTransaction + + hash atomic.Value // Hash of the typed QKC transaction envelope. +} + +func (tx *Transaction) CopyEvmTx() (*Transaction, error) { + data, err := serialize.SerializeToBytes(tx) + if err != nil { + return nil, err + } + var evmTx Transaction + err = serialize.DeserializeFromBytes(data, &evmTx) + if err != nil { + return nil, err + } + return &evmTx, nil +} + +func (tx *Transaction) Serialize(w *[]byte) error { + *w = append(*w, tx.TxType) + + switch tx.TxType { + case EvmTx: + bytes, err := rlp.EncodeToBytes(tx.EvmTx) + if err != nil { + return err + } + serialize.Serialize(w, uint32(len(bytes))) + *w = append(*w, bytes...) + return nil + default: + return fmt.Errorf("ser: Transacton type %d is not supported", tx.TxType) + } +} + +func (tx *Transaction) Deserialize(bb *serialize.ByteBuffer) error { + txType, err := bb.GetUInt8() + if err != nil { + return err + } + + switch txType { + case EvmTx: + tx.TxType = txType + bytes, err := bb.GetVarBytes(4) + if err != nil { + return err + } + + if tx.EvmTx == nil { + tx.EvmTx = new(EvmTransaction) + } + return rlp.DecodeBytes(bytes, tx.EvmTx) + default: + return fmt.Errorf("deser: Transacton type %d is not supported", txType) + } +} + +// Hash return the hash of the transaction it contained +func (tx *Transaction) Hash() (h common.Hash) { + if tx.TxType == EvmTx { + if hash := tx.hash.Load(); hash != nil { + return hash.(common.Hash) + } + hw := crypto.NewKeccakState() + serialTxBytes, err := serialize.SerializeToBytes(tx) + if err != nil { + //TODO panic ? + //TODO not cache? + panic(err) + } + hw.Write(serialTxBytes) + hw.Sum(h[:0]) + tx.hash.Store(h) + return h + } + + log.Error(fmt.Sprintf("do not support tx type %d", tx.TxType)) + return *new(common.Hash) +} + +func (tx *Transaction) getNonce() uint64 { + if tx.TxType == EvmTx { + return tx.EvmTx.data.AccountNonce + } + + //todo verify the default value when have more type of tx + return 0 +} + +func (tx *Transaction) getPrice() *big.Int { + if tx.TxType == EvmTx { + return tx.EvmTx.data.Price + } + + //todo verify the default value when have more type of tx + return big.NewInt(0) +} + +func (tx *Transaction) Sender(signer Signer) (account.Recipient, error) { + if tx.TxType == EvmTx { + addr, err := Sender(signer, tx.EvmTx) + if err != nil { + log.Error(err.Error(), "tx", tx) + return account.Recipient{}, err + } + + return addr, nil + } else { + err := errors.New(fmt.Sprintf("do not support tx type %d", tx.TxType)) + log.Error(err.Error()) + return account.Recipient{}, err + } +} + +// Transactions is a EvmTransaction slice type for basic sorting. +type Transactions []*Transaction + +// Len returns the length of s. +func (s Transactions) Len() int { return len(s) } + +func (s Transactions) Bytes(i int) []byte { + enc, err := serialize.SerializeToBytes(s[i]) + if err != nil { + panic(err) + } + return enc +} + +// Swap swaps the i'th and the j'th element in s. +func (s Transactions) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// TxDifference returns a new set which is the difference between a and b. +func TxDifference(a, b Transactions) Transactions { + keep := make(Transactions, 0, len(a)) + + remove := make(map[common.Hash]struct{}) + for _, tx := range b { + remove[tx.Hash()] = struct{}{} + } + + for _, tx := range a { + if _, ok := remove[tx.Hash()]; !ok { + keep = append(keep, tx) + } + } + + return keep +} diff --git a/qkc/types/transaction_signing.go b/qkc/types/transaction_signing.go new file mode 100644 index 000000000000..1d15f005ccd5 --- /dev/null +++ b/qkc/types/transaction_signing.go @@ -0,0 +1,196 @@ +// Copyright 2026-2027, QuarkChain. + +// Transaction signing follows pyquarkchain-compatible QKC signing semantics. +// Modified from go-ethereum under GNU Lesser General Public License + +package types + +import ( + "crypto/ecdsa" + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/qkc/account" +) + +var ( + ErrInvalidNetworkID = errors.New("invalid network ID for signer") +) + +// sigCache is used to cache the derived sender and contains +// the signer used to derive it. +type sigCache struct { + signer Signer + from account.Recipient +} + +// MakeSigner returns a signer with the expected QKC network and Ethereum chain IDs. +func MakeSigner(qkcNetworkID, ethChainID uint32) Signer { + return NewQKCSigner(qkcNetworkID, ethChainID) +} + +// SignTx signs the transaction using the given signer and private key +func SignTx(tx *EvmTransaction, s Signer, prv *ecdsa.PrivateKey) (*EvmTransaction, error) { + h := s.Hash(tx) + sig, err := crypto.Sign(h[:], prv) + if err != nil { + return nil, err + } + return tx.WithSignature(s, sig) +} + +// Sender returns the address derived from the signature (V, R, S) using secp256k1 +// elliptic curve and an error if it failed deriving or upon an incorrect +// signature. +// +// Sender may cache the address, allowing it to be used regardless of +// signing method. The cache is invalidated if the cached signer does +// not match the signer used in the current call. +func Sender(signer Signer, tx *EvmTransaction) (account.Recipient, error) { + if sc := tx.from.Load(); sc != nil { + sigCache := sc.(sigCache) + // If the signer used to derive from in a previous + // call is not the same as used current, invalidate + // the cache. + if sigCache.signer.Equal(signer) { + return sigCache.from, nil + } + } + + addr, err := signer.Sender(tx) + if err != nil { + return account.Recipient{}, err + } + tx.from.Store(sigCache{signer: signer, from: addr}) + return addr, nil +} + +// Signer encapsulates transaction signature handling. Note that this interface is not a +// stable API and may change at any time to accommodate new protocol rules. +type Signer interface { + // Sender returns the sender address of the transaction. + Sender(tx *EvmTransaction) (account.Recipient, error) + // SignatureValues returns the raw R, S, V values corresponding to the + // given signature. + SignatureValues(tx *EvmTransaction, sig []byte) (r, s, v *big.Int, err error) + // Hash returns the hash to be signed. + Hash(tx *EvmTransaction) common.Hash + // Equal returns true if the given signer is the same as the receiver. + Equal(Signer) bool +} + +// QKCSigner implements QKC transaction signature rules for all supported versions. +// Version 0 and 1 use qkcNetworkID, while version 2 uses ethChainID. +type QKCSigner struct { + qkcNetworkID uint32 + ethChainID uint32 +} + +func NewQKCSigner(qkcNetworkID, ethChainID uint32) QKCSigner { + return QKCSigner{ + qkcNetworkID: qkcNetworkID, + ethChainID: ethChainID, + } +} + +func (s QKCSigner) Equal(s2 Signer) bool { + other, ok := s2.(QKCSigner) + return ok && other.qkcNetworkID == s.qkcNetworkID && other.ethChainID == s.ethChainID +} + +func (s QKCSigner) Sender(tx *EvmTransaction) (account.Recipient, error) { + switch tx.Version() { + case 0: + if tx.NetworkId() != s.qkcNetworkID { + return account.Recipient{}, ErrInvalidNetworkID + } + return recoverPlain(tx.getUnsignedHash(), tx.data.R, tx.data.S, tx.data.V, true) + case 1: + if tx.NetworkId() != s.qkcNetworkID { + return account.Recipient{}, ErrInvalidNetworkID + } + hashTyped, err := tx.typedHash() + if err != nil { + return account.Recipient{}, err + } + return recoverPlain(hashTyped, tx.data.R, tx.data.S, tx.data.V, true) + case 2: + if tx.NetworkId() != s.ethChainID { + return account.Recipient{}, ErrInvalidNetworkID + } + chainIDMul := new(big.Int).Mul(big.NewInt(int64(s.ethChainID)), big.NewInt(2)) + V := new(big.Int).Sub(tx.data.V, chainIDMul) + V.Sub(V, big.NewInt(8)) + sender, err := recoverPlain(tx.getUnsignedHashForEip155(s.ethChainID), tx.data.R, tx.data.S, V, true) + return sender, err + default: + return account.Recipient{}, fmt.Errorf("unsupported transaction version %d", tx.Version()) + } +} + +// SignatureValues returns signature values. This signature +// needs to be in the [R || S || V] format where V is 0 or 1. +func (s QKCSigner) SignatureValues(tx *EvmTransaction, sig []byte) (R, S, V *big.Int, err error) { + if len(sig) != 65 { + panic(fmt.Sprintf("wrong size for signature: got %d, want 65", len(sig))) + } + R = new(big.Int).SetBytes(sig[:32]) + S = new(big.Int).SetBytes(sig[32:64]) + V = new(big.Int).SetBytes([]byte{sig[64] + 27}) + if tx.Version() == 2 { + V.Add(V, new(big.Int).SetUint64(8+2*uint64(tx.NetworkId()))) + } + + return R, S, V, nil +} + +// Hash returns the hash to be signed by the sender. +// It does not uniquely identify the transaction. +func (s QKCSigner) Hash(tx *EvmTransaction) common.Hash { + switch tx.Version() { + case 0: + return tx.getUnsignedHash() + case 1: + hash, err := tx.typedHash() + if err != nil { + panic(fmt.Sprintf("typed transaction hash: %v", err)) + } + return hash + case 2: + return tx.getUnsignedHashForEip155(s.ethChainID) + default: + panic(fmt.Sprintf("unsupported transaction version %d", tx.Version())) + } +} + +func recoverPlain(sighash common.Hash, R, S, Vb *big.Int, homestead bool) (account.Recipient, error) { + if Vb.BitLen() > 8 { + return account.Recipient{}, ErrInvalidSig + } + // QuarkChain use NetworkId to store the chain Id instead of added to V, + // so do not need to remove chain Id from VB + V := byte(Vb.Uint64() - 27) + if !crypto.ValidateSignatureValues(V, R, S, homestead) { + return account.Recipient{}, ErrInvalidSig + } + // encode the signature in uncompressed format + r, s := R.Bytes(), S.Bytes() + sig := make([]byte, 65) + copy(sig[32-len(r):32], r) + copy(sig[64-len(s):64], s) + sig[64] = V + // recover the public key from the signature + pub, err := crypto.Ecrecover(sighash[:], sig) + if err != nil { + return account.Recipient{}, err + } + if len(pub) == 0 || pub[0] != 4 { + return account.Recipient{}, errors.New("invalid public key") + } + var addr account.Recipient + copy(addr[:], crypto.Keccak256(pub[1:])[12:]) + return addr, nil +} diff --git a/qkc/types/transaction_signing_test.go b/qkc/types/transaction_signing_test.go new file mode 100644 index 000000000000..374c39c67a60 --- /dev/null +++ b/qkc/types/transaction_signing_test.go @@ -0,0 +1,128 @@ +// Copyright 2026-2027, QuarkChain. + +// Transaction signing tests exercise pyquarkchain-compatible QKC signatures. +// +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package types + +import ( + "errors" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/qkc/account" +) + +func TestQKCSigning(t *testing.T) { + key, _ := crypto.GenerateKey() + recipient := publicKey2Recipient(&key.PublicKey) + + signer := NewQKCSigner(1, 1) + tx, err := SignTx(NewEvmTransaction(0, recipient, new(big.Int), 0, new(big.Int), 0, 0, 1, 0, nil, 0, 0), signer, key) + if err != nil { + t.Fatal(err) + } + + from, err := Sender(signer, tx) + if err != nil { + t.Fatal(err) + } + if from != recipient { + t.Errorf("exected from and address to be equal. Got %x want %x", from, recipient) + } +} + +func TestTypedTransactionSigning(t *testing.T) { + key, _ := crypto.GenerateKey() + recipient := publicKey2Recipient(&key.PublicKey) + signer := NewQKCSigner(1, 1) + tx, err := SignTx(NewEvmTransaction(0, recipient, new(big.Int), 0, new(big.Int), 0, 0, 1, 1, nil, 0, 0), signer, key) + if err != nil { + t.Fatal(err) + } + + from, err := Sender(signer, tx) + if err != nil { + t.Fatal(err) + } + if from != recipient { + t.Errorf("expected sender %x, got %x", recipient, from) + } +} + +func TestEIP155TransactionSigning(t *testing.T) { + key, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + recipient := publicKey2Recipient(&key.PublicKey) + const chainID = uint32(3) + signer := NewQKCSigner(1, chainID) + tx, err := SignTx(NewEvmTransaction(0, recipient, new(big.Int), 0, new(big.Int), 0, 0, chainID, 2, nil, 0, 0), signer, key) + if err != nil { + t.Fatal(err) + } + + v, _, _ := tx.RawSignatureValues() + base := uint64(35 + 2*chainID) + if got := v.Uint64(); got != base && got != base+1 { + t.Fatalf("unexpected EIP-155 V %d, want %d or %d", got, base, base+1) + } + from, err := Sender(signer, tx) + if err != nil { + t.Fatal(err) + } + if from != recipient { + t.Errorf("expected sender %x, got %x", recipient, from) + } +} + +func TestQKCSignerHashForVersion2(t *testing.T) { + key, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + tx := NewEvmTransaction(0, publicKey2Recipient(&key.PublicKey), new(big.Int), 0, new(big.Int), 0, 0, 1, 2, nil, 0, 0) + signer := NewQKCSigner(1, tx.NetworkId()) + + if got, want := signer.Hash(tx), tx.getUnsignedHashForEip155(tx.NetworkId()); got != want { + t.Errorf("EIP-155 hash mismatch, got %x want %x", got, want) + } +} + +func TestQKCSignerRejectsWrongNetworkID(t *testing.T) { + tests := []struct { + name string + version uint32 + network uint32 + signer QKCSigner + }{ + {"qkc", 0, 2, NewQKCSigner(1, 3)}, + {"eip-155", 2, 4, NewQKCSigner(1, 3)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tx := NewEvmTransaction(0, account.Recipient{}, new(big.Int), 0, new(big.Int), 0, 0, test.network, test.version, nil, 0, 0) + _, err := Sender(test.signer, tx) + if !errors.Is(err, ErrInvalidNetworkID) { + t.Fatalf("expected ErrInvalidNetworkID, got %v", err) + } + }) + } +} diff --git a/qkc/types/transaction_test.go b/qkc/types/transaction_test.go new file mode 100644 index 000000000000..d2a04bd74a2b --- /dev/null +++ b/qkc/types/transaction_test.go @@ -0,0 +1,260 @@ +// Copyright 2026-2027, QuarkChain. + +// Transaction tests exercise pyquarkchain-compatible QKC wire bytes. + +package types + +import ( + "bytes" + "crypto/ecdsa" + "encoding/hex" + "math/big" + "reflect" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/qkc/account" + "github.com/ethereum/go-ethereum/qkc/serialize" + "github.com/ethereum/go-ethereum/rlp" +) + +// The values in those tests are from the EvmTransaction Tests +var ( + reciept = account.BytesToIdentityRecipient(common.Hex2Bytes("b94f5374fce5edbc8e2a8697c15331677e6ebf0b")) + emptyEvmTx = NewEvmTransaction( + 0, + reciept, + big.NewInt(0), 0, big.NewInt(0), + 0, 0, 1, 0, nil, 0, 0, + ) + emptyTx = Transaction{TxType: 0, EvmTx: emptyEvmTx} + //nonce , to , amount , gasLimit , gasPrice, fromFullShardKey , toFullShardKey , networkId , version , data + rightvrsTx = NewEvmTransaction( + 3, + reciept, + big.NewInt(10), + 2000, + big.NewInt(1), + 0, + 0, + 1, + 0, + nil, 0, 0, + ) + signTx, _ = rightvrsTx.WithSignature( + NewQKCSigner(1, 1), + common.Hex2Bytes("98ff921201554726367d2be8c804a7ff89ccf285ebc57dff8ae4c44b9c19ac4a8887321be575c8095f789dd4c743dfe42c1820f9231f98a962b210e3ac2452a301"), + ) +) + +func TestTransactionSigHash(t *testing.T) { + var signer = NewQKCSigner(1, 1) + //hash unsigned + if signer.Hash(emptyEvmTx) != common.HexToHash("15e523e4a18884f01753358af140664007e19b2c67cfa6618cadb85de14f3bd0") { + t.Errorf("empty transaction unsigned hash mismatch, got %x, expect %x", signer.Hash(emptyEvmTx), common.HexToHash("297d6ae9803346cdb059a671dea7e37b684dcabfa767f2d872026ad0a3aba495")) + } + if emptyEvmTx.Hash() != common.HexToHash("a04873d41928c8acc76d4d6495fec31fb58afc7d5a5782d9ba4bb30fdbf1b147") { + t.Errorf("empty transaction hash mismatch, got %x, expect %x", emptyTx.Hash(), common.HexToHash("a40920ae6f758f88c61b405f9fc39fdd6274666462b14e3887522166e6537a97")) + } + + //hash unsigned + if signer.Hash(rightvrsTx) != common.HexToHash("a8915d9a38bacbdc640ab287d4beb9b06ea1af52da8568c298739c9d7514e87b") { + t.Errorf("RightVRS transaction unsigned hash mismatch, got %x, expect %x", signer.Hash(rightvrsTx), common.HexToHash("e4f3c1dd000045bf26006df7eb7cb0a882f70a6ab81723d93638151f6418f78a")) + } + if rightvrsTx.Hash() != common.HexToHash("4bf87b2a5b39b7894b4b4b197ffe1ef7e67085bbc60d599ed3d4d587aa72af76") { + t.Errorf("RightVRS transaction hash mismatch, got %x, expect %x", rightvrsTx.Hash(), common.HexToHash("df227f34313c2bc4a4a986817ea46437f049873f2fca8e2b89b1ecd0f9e67a28")) + } +} + +func TestEvmTransactionHashInvalidatedBySetters(t *testing.T) { + newTx := func() *EvmTransaction { + return NewEvmTransaction(0, reciept, big.NewInt(0), 0, big.NewInt(0), 0, 0, 1, 0, nil, 0, 0) + } + tests := []struct { + name string + set func(*EvmTransaction) + }{ + {"gas", func(tx *EvmTransaction) { tx.SetGas(1) }}, + {"from full shard key", func(tx *EvmTransaction) { tx.SetFromFullShardKey(1) }}, + {"nonce", func(tx *EvmTransaction) { tx.SetNonce(1) }}, + {"signature", func(tx *EvmTransaction) { tx.SetVRS(big.NewInt(27), big.NewInt(1), big.NewInt(1)) }}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tx := newTx() + before := tx.Hash() + test.set(tx) + if got := tx.Hash(); got == before { + t.Fatal("hash was not invalidated") + } + }) + } +} + +func TestSetSenderUsesProvidedSigner(t *testing.T) { + tx := NewEvmTransaction(0, reciept, big.NewInt(0), 0, big.NewInt(0), 0, 0, 3, 2, nil, 0, 0) + signer := NewQKCSigner(1, 3) + want := account.BytesToIdentityRecipient([]byte{1}) + tx.SetSender(signer, want) + + got, err := Sender(signer, tx) + if err != nil { + t.Fatal(err) + } + if got != want { + t.Errorf("cached sender mismatch: got %x, want %x", got, want) + } +} + +func TestTransactionEncode(t *testing.T) { + txb, err := rlp.EncodeToBytes(rightvrsTx) + if err != nil { + t.Fatalf("encode error: %v", err) + } + + should := common.FromHex("ed03018207d094b94f5374fce5edbc8e2a8697c15331677e6ebf0b0a800184000000008400000000808080808080") + if !bytes.Equal(txb, should) { + t.Errorf("encoded RLP mismatch, got %x", txb) + } +} + +func decodeTx(data []byte) (*EvmTransaction, error) { + var tx EvmTransaction + t, err := &tx, rlp.Decode(bytes.NewReader(data), &tx) + + return t, err +} + +func publicKey2Recipient(pk *ecdsa.PublicKey) account.Recipient { + pubBytes := crypto.FromECDSAPub(pk) + recipient := account.BytesToIdentityRecipient(crypto.Keccak256(pubBytes[1:])[12:]) + return recipient +} + +func defaultTestKey() (*ecdsa.PrivateKey, account.Recipient) { + key, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8") + recipient := publicKey2Recipient(&key.PublicKey) + return key, recipient +} + +func TestRecipientEmpty(t *testing.T) { + _, addr := defaultTestKey() + tx, err := decodeTx(common.Hex2Bytes("f86b80808094b94f5374fce5edbc8e2a8697c15331677e6ebf0b808001840000000084000000008080801ba0d7265f92d763da5e2ea5016b837bf56f5bf42d22aead9ad5e7be2ddf01efcc68a07159634972d77349a76108c6db0634ea7b65768881b152c656deca190df6e427")) + if err != nil { + t.Error(err) + t.FailNow() + } + + from, err := Sender(NewQKCSigner(tx.NetworkId(), tx.NetworkId()), tx) + if err != nil { + t.Error(err) + t.FailNow() + } + if addr != from { + t.Errorf("derived address doesn't match addr %x, from %x", addr, from) + } +} + +func TestRecipientNormal(t *testing.T) { + _, addr := defaultTestKey() + + tx, err := decodeTx(common.Hex2Bytes("f86b80808094b94f5374fce5edbc8e2a8697c15331677e6ebf0b808001840000000084000000008080801ba0d7265f92d763da5e2ea5016b837bf56f5bf42d22aead9ad5e7be2ddf01efcc68a07159634972d77349a76108c6db0634ea7b65768881b152c656deca190df6e427")) + if err != nil { + t.Error(err) + t.FailNow() + } + + from, err := Sender(NewQKCSigner(1, 1), tx) + if err != nil { + t.Error(err) + t.FailNow() + } + + if addr != from { + t.Error("derived address doesn't match") + } +} + +func TestTxSize(t *testing.T) { + + id1, err := account.CreatRandomIdentity() + if err != nil { + t.Fatal("CreatIdentityFromKey error: ", err) + } + defaultFullShardKey, err := id1.GetDefaultFullShardKey() + if err != nil { + t.Fatal("GetDefaultFullShardKey error: ", err) + } + acc1 := account.CreatAddressFromIdentity(id1, defaultFullShardKey) + check := func(f string, got, want interface{}) { + if !reflect.DeepEqual(got, want) { + t.Errorf("%s mismatch: got %v, want %v", f, got, want) + } + } + evmTx := NewEvmTransaction( + 0, + acc1.Recipient, + big.NewInt(0), + 30000, + big.NewInt(0), + 0xFFFF, + 0xFFFF, + 1, + 0, + nil, + 12345, + 1234, + ) + signer := NewQKCSigner(1, 1) + prvKey, err := crypto.HexToECDSA(hex.EncodeToString(id1.GetKey().Bytes())) + if err != nil { + t.Fatal("prvKey error: ", err) + } + evmTx, err = SignTx(evmTx, signer, prvKey) + if err != nil { + t.Fatal("SignTx error: ", err) + } + tx := &Transaction{ + EvmTx: evmTx, + TxType: EvmTx, + } + txBytes, err := serialize.SerializeToBytes(&tx) + if err != nil { + t.Fatal("Serialize error: ", err) + } + + TT256 := new(big.Int).Sub(new(big.Int).Exp(big.NewInt(2), big.NewInt(256), big.NewInt(0)), big.NewInt(1)) + SHARD_KEY_MAX := new(big.Int).Exp(big.NewInt(256), big.NewInt(4), big.NewInt(0)) + TOKEN_ID_MAX, _ := new(big.Int).SetString("4873763662273663091", 10) + evmTx2 := NewEvmTransaction( + TT256.Uint64(), + acc1.Recipient, + TT256, + TT256.Uint64(), + TT256, + uint32(SHARD_KEY_MAX.Uint64()), + uint32(SHARD_KEY_MAX.Uint64()), + 1, + 0, + []byte{0}, + TOKEN_ID_MAX.Uint64(), + TOKEN_ID_MAX.Uint64(), + ) + + evmTx2, err = SignTx(evmTx2, signer, prvKey) + if err != nil { + t.Fatal("SignTx error: ", err) + } + tx2 := &Transaction{ + EvmTx: evmTx2, + TxType: EvmTx, + } + txBytes2, err := serialize.SerializeToBytes(&tx2) + if err != nil { + t.Fatal("Serialize error: ", err) + } + + check("EvmTransaction min len", len(txBytes), 120) + check("EvmTransaction max len", len(txBytes2), 210) +} diff --git a/qkc/types/transaction_typed_hash.go b/qkc/types/transaction_typed_hash.go new file mode 100644 index 000000000000..9ae3dc2825d9 --- /dev/null +++ b/qkc/types/transaction_typed_hash.go @@ -0,0 +1,244 @@ +// Copyright 2026-2027, QuarkChain. + +// Transaction typed-hash helpers follow pyquarkchain-compatible QKC signing. + +package types + +import ( + "encoding/hex" + "errors" + "fmt" + "math/big" + "regexp" + "strconv" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/qkc/account" +) + +func bigIntToHex(data *big.Int) string { + return hexutil.Encode(data.Bytes()) +} + +func uint64ToHex(data uint64) string { + return hexutil.Encode(new(big.Int).SetUint64(data).Bytes()) +} + +func uint32ToHex(data uint32) string { + return hexutil.Encode(new(big.Int).SetUint64(uint64(data)).Bytes()) +} + +func recipientToHex(data *account.Recipient) string { + if data == nil { + return "0x" + } + return strings.ToLower(data.String()) +} + +func strRJust(initStr []byte, fill byte, width int) []byte { + if len(initStr) >= width { + return initStr + } + data := make([]byte, 0) + for index := 0; index < width-len(initStr); index++ { + data = append(data, fill) + } + data = append(data, initStr...) + return data +} + +func evmTxToTypedData(evmTx *EvmTransaction) []map[string]string { + typedTxData := make([]map[string]string, 0) + typedTxData = append(typedTxData, map[string]string{ + "type": "uint256", + "name": "nonce", + "value": uint64ToHex(evmTx.data.AccountNonce), + }) + typedTxData = append(typedTxData, map[string]string{ + "type": "uint256", + "name": "gasPrice", + "value": bigIntToHex(evmTx.data.Price), + }) + typedTxData = append(typedTxData, map[string]string{ + "type": "uint256", + "name": "gasLimit", + "value": uint64ToHex(evmTx.data.GasLimit), + }) + typedTxData = append(typedTxData, map[string]string{ + "type": "uint160", + "name": "to", + "value": recipientToHex(evmTx.To()), + }) + typedTxData = append(typedTxData, map[string]string{ + "type": "uint256", + "name": "value", + "value": bigIntToHex(evmTx.data.Amount), + }) + typedTxData = append(typedTxData, map[string]string{ + "type": "bytes", + "name": "data", + "value": "0x" + hex.EncodeToString(evmTx.data.Payload), + }) + typedTxData = append(typedTxData, map[string]string{ + "type": "uint256", + "name": "networkId", + "value": uint32ToHex(evmTx.data.NetworkId), + }) + typedTxData = append(typedTxData, map[string]string{ + "type": "uint32", + "name": "fromFullShardKey", + "value": uint32ToHex(evmTx.data.FromFullShardKey.GetValue()), + }) + typedTxData = append(typedTxData, map[string]string{ + "type": "uint32", + "name": "toFullShardKey", + "value": uint32ToHex(evmTx.data.ToFullShardKey.GetValue()), + }) + typedTxData = append(typedTxData, map[string]string{ + "type": "uint64", + "name": "gasTokenId", + "value": uint64ToHex(evmTx.data.GasTokenID), + }) + typedTxData = append(typedTxData, map[string]string{ + "type": "uint64", + "name": "transferTokenId", + "value": uint64ToHex(evmTx.data.TransferTokenID), + }) + typedTxData = append(typedTxData, map[string]string{ + "type": "string", + "name": "qkcDomain", + "value": "bottom-quark", + }) + return typedTxData +} + +func calSize(str string) (int, error) { + reg := regexp.MustCompile(`\d+`) + indexList := reg.FindAllStringIndex(str, -1) + if len(indexList) != 1 { + return 0, errors.New("len(indexList) should equal 1") + } + data := str[indexList[0][0]:indexList[0][1]] + size, err := strconv.ParseInt(data, 10, 64) + if err != nil { + return 0, err + } + return int(size), nil +} + +func solidityPack(types, values []string) ([]byte, error) { + if len(types) != len(values) { + return nil, errors.New("types's len should equal values's len") + } + retv := make([]byte, 0) + for index, t := range types { + value := values[index] + if t == "bytes" { + if value == "0x" { + continue + } + //TODO:add test for payload + d := common.FromHex(value) + retv = append(retv, d...) + + } else if t == "string" { + retv = append(retv, []byte(value)...) + + } else if t == "bool" || t == "address" { + return nil, errors.New("not support bool and address") + + } else if strings.HasPrefix(t, "bytes") { + size, err := calSize(t) + if err != nil { + return nil, err + } + if size < 1 || size > 32 { + return nil, errors.New("unsupported byte size") + } + v, err := hex.DecodeString(value[2:]) + if len(v) > size { + return nil, errors.New("data is large than size") + } + retv = append(retv, []byte(strRJust(v, byte(0), size))...) + + } else if strings.HasPrefix(t, "int") || strings.HasPrefix(t, "uint") { + size, err := calSize(t) + if err != nil { + return nil, err + } + if size%8 != 0 || size < 8 || size > 256 { + return nil, errors.New("unsupported int size") + } + v, err := hex.DecodeString(value[2:]) + if err != nil { + return nil, err + } + if len(v) > int(size)/8 { + return nil, errors.New("data is larger than size") + } + retv = append(retv, []byte(strRJust(v, byte(0), int(size)/8))...) + } else { + return nil, fmt.Errorf("unsupported or invalid type %v", t) + } + } + return retv, nil +} + +func schema(tx []map[string]string) []string { + t := make([]string, 0) + for _, v := range tx { + t = append(t, fmt.Sprintf("%s %s", v["type"], v["name"])) + } + return t +} + +func types(tx []map[string]string) []string { + t := make([]string, 0) + for _, v := range tx { + t = append(t, v["type"]) + } + return t +} + +func data(tx []map[string]string) []string { + t := make([]string, 0) + for _, v := range tx { + if v["type"] == "bytes" { + t = append(t, v["value"][2:]) + } else { + t = append(t, v["value"]) + } + } + return t +} + +func typedSignatureHash(tx []map[string]string) (string, error) { + schema := schema(tx) + types := types(tx) + data := data(tx) + + string1 := make([]string, 0) + for index := 0; index < len(tx); index++ { + string1 = append(string1, "string") + } + + s1, err := soliditySha3(string1, schema) + if err != nil { + return "", err + } + s2, err := soliditySha3(types, data) + if err != nil { + return "", err + } + return soliditySha3([]string{"bytes32", "bytes32"}, []string{s1, s2}) +} + +func soliditySha3(types, value []string) (string, error) { + packData, err := solidityPack(types, value) + if err != nil { + return common.Hash{}.String(), err + } + return sha3_256(packData).String(), nil +} diff --git a/qkc/types/transaction_typed_hash_test.go b/qkc/types/transaction_typed_hash_test.go new file mode 100644 index 000000000000..660d14636aac --- /dev/null +++ b/qkc/types/transaction_typed_hash_test.go @@ -0,0 +1,167 @@ +// Copyright 2026-2027, QuarkChain. + +// Transaction typed-hash tests exercise pyquarkchain-compatible QKC signing inputs. + +package types + +import ( + "encoding/hex" + "math/big" + "strconv" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" +) + +func bytesToUint64(data string) uint64 { + data = data[2:] + n, err := strconv.ParseUint(data, 16, 64) + if err != nil { + panic(err) + } + + n2 := uint64(n) + return n2 +} + +func bytesToUint32(data string) uint32 { + data = data[2:] + n, err := strconv.ParseUint(data, 16, 32) + if err != nil { + panic(err) + } + + n2 := uint32(n) + return n2 +} +func bytesToBigInt(data string) *big.Int { + n := new(big.Int) + n, _ = n.SetString(data[2:], 16) + + return n +} + +var ( + rawEvmTx = NewEvmTransaction(bytesToUint64("0x0d"), common.BytesToAddress(common.FromHex("314b2cd22c6d26618ce051a58c65af1253aecbb8")), + bytesToBigInt("0x056bc75e2d63100000"), bytesToUint64("0x7530"), bytesToBigInt("0x02540be400"), bytesToUint32("0xc47decfd"), + bytesToUint32("0xc49c1950"), bytesToUint32("0x03"), 1, nil, bytesToUint64("0x0111"), bytesToUint64("0x0222"), + ) + rawTx = Transaction{ + TxType: EvmTx, + EvmTx: rawEvmTx, + } + + tx = []map[string]string{ + { + "type": "uint256", + "name": "nonce", + "value": "0x0d", + }, + { + "type": "uint256", + "name": "gasPrice", + "value": "0x02540be400", + }, + { + "type": "uint256", + "name": "gasLimit", + "value": "0x7530", + }, + { + "type": "uint160", + "name": "to", + "value": "0x314b2cd22c6d26618ce051a58c65af1253aecbb8", + }, + { + "type": "uint256", + "name": "value", + "value": "0x056bc75e2d63100000", + }, + { + "type": "bytes", + "name": "data", + "value": "0x", + }, + { + "type": "uint256", + "name": "networkId", + "value": "0x03", + }, + { + "type": "uint32", + "name": "fromFullShardKey", + "value": "0xc47decfd", + }, + { + "type": "uint32", + "name": "toFullShardKey", + "value": "0xc49c1950", + }, + { + "type": "uint64", + "name": "gasTokenId", + "value": "0x0111", + }, + { + "type": "uint64", + "name": "transferTokenId", + "value": "0x0222", + }, + { + "type": "string", + "name": "qkcDomain", + "value": "bottom-quark", + }, + } +) + +func TestTyped(t *testing.T) { + assert.Equal(t, tx, evmTxToTypedData(rawEvmTx)) +} + +func TestSolidityPack(t *testing.T) { + schema := schema(tx) + types := types(tx) + data := data(tx) + + t1 := make([]string, 0) + for index := 0; index < len(tx); index++ { + t1 = append(t1, "string") + } + h1, err := solidityPack(t1, schema) + assert.NoError(t, err) + assert.Equal(t, hex.EncodeToString(h1), "75696e74323536206e6f6e636575696e7432353620676173507269636575696e74323536206761734c696d697475696e7431363020746f75696e743235362076616c75656279746573206461746175696e74323536206e6574776f726b496475696e7433322066726f6d46756c6c53686172644b657975696e74333220746f46756c6c53686172644b657975696e74363420676173546f6b656e496475696e743634207472616e73666572546f6b656e4964737472696e6720716b63446f6d61696e") + + h2, err := solidityPack(types, data) + assert.NoError(t, err) + assert.Equal(t, hex.EncodeToString(h2), "000000000000000000000000000000000000000000000000000000000000000d00000000000000000000000000000000000000000000000000000002540be4000000000000000000000000000000000000000000000000000000000000007530314b2cd22c6d26618ce051a58c65af1253aecbb80000000000000000000000000000000000000000000000056bc75e2d631000000000000000000000000000000000000000000000000000000000000000000003c47decfdc49c195000000000000001110000000000000222626f74746f6d2d717561726b") +} + +func TestTypedSignatureHash(t *testing.T) { + h, err := typedSignatureHash(tx) + assert.NoError(t, err) + assert.Equal(t, h, "0xe768719d0a211ffb0b7f9c7bc6af9286136b3dd8b6be634a57dc9d6bee35b492") +} + +func TestTypedSignatureHashWithPayload(t *testing.T) { + typedTx := append([]map[string]string(nil), tx...) + typedTx[5] = map[string]string{ + "type": "bytes", + "name": "data", + "value": "0xdeadbeef", + } + + h, err := typedSignatureHash(typedTx) + assert.NoError(t, err) + assert.Equal(t, h, "0x3816812fc28bba89fbadfec978d42ba11df564c7b39712987aa1a9abc2f50380") +} + +func TestRecover(t *testing.T) { + rawTx.EvmTx.SetVRS(bytesToBigInt("0x1b"), bytesToBigInt("0xb5145678e43df2b7ea8e0e969e51dbf72c956dd52e234c95393ad68744394855"), bytesToBigInt("0x44515b465dbbf746a484239c11adb98f967e35347e17e71b84d850d8e5c38a6a")) + + sender, err := Sender(NewQKCSigner(rawTx.EvmTx.NetworkId(), rawTx.EvmTx.NetworkId()), rawTx.EvmTx) + assert.NoError(t, err) + assert.Equal(t, strings.ToLower(sender.String()[2:]), "2e6144d0a4786e6f62892eee59c24d1e81e33272") +}