diff --git a/parser/transaction.go b/parser/transaction.go index add99d15..13a8d3f9 100644 --- a/parser/transaction.go +++ b/parser/transaction.go @@ -7,6 +7,7 @@ package parser import ( "crypto/sha256" + "fmt" "github.com/pkg/errors" "github.com/zcash/lightwalletd/parser/internal/bytestring" @@ -14,20 +15,21 @@ import ( ) type rawTransaction struct { - fOverwintered bool - version uint32 - nVersionGroupID uint32 - transparentInputs []*txIn - transparentOutputs []*txOut - nLockTime uint32 - nExpiryHeight uint32 - valueBalance int64 - shieldedSpends []*spend - shieldedOutputs []*output - joinSplits []*joinSplit - joinSplitPubKey []byte - joinSplitSig []byte - bindingSig []byte + fOverwintered bool + version uint32 + nVersionGroupID uint32 + consensusBranchID uint32 + transparentInputs []*txIn + transparentOutputs []*txOut + nLockTime uint32 + nExpiryHeight uint32 + valueBalanceSapling int64 + shieldedSpends []*spend + shieldedOutputs []*output + joinSplits []*joinSplit + joinSplitPubKey []byte + joinSplitSig []byte + bindingSigSapling []byte } // Txin format as described in https://en.bitcoin.it/wiki/Transaction @@ -90,8 +92,42 @@ func (tx *txOut) ParseFromSlice(data []byte) ([]byte, error) { return []byte(s), nil } +// parse the transparent parts of the transaction +func (tx *Transaction) ParseTransparent(data []byte) ([]byte, error) { + s := bytestring.String(data) + var txInCount int + if !s.ReadCompactSize(&txInCount) { + return nil, errors.New("could not read tx_in_count") + } + var err error + tx.transparentInputs = make([]*txIn, txInCount) + for i := 0; i < txInCount; i++ { + ti := &txIn{} + s, err = ti.ParseFromSlice([]byte(s)) + if err != nil { + return nil, errors.Wrap(err, "while parsing transparent input") + } + tx.transparentInputs[i] = ti + } + + var txOutCount int + if !s.ReadCompactSize(&txOutCount) { + return nil, errors.New("could not read tx_out_count") + } + tx.transparentOutputs = make([]*txOut, txOutCount) + for i := 0; i < txOutCount; i++ { + to := &txOut{} + s, err = to.ParseFromSlice([]byte(s)) + if err != nil { + return nil, errors.Wrap(err, "while parsing transparent output") + } + tx.transparentOutputs[i] = to + } + return []byte(s), nil +} + // spend is a Sapling Spend Description as described in 7.3 of the Zcash -// protocol spec. Total size is 384 bytes. +// protocol specification. type spend struct { cv []byte // 32 anchor []byte // 32 @@ -101,14 +137,14 @@ type spend struct { spendAuthSig []byte // 64 } -func (p *spend) ParseFromSlice(data []byte) ([]byte, error) { +func (p *spend) ParseFromSlice(data []byte, version uint32) ([]byte, error) { s := bytestring.String(data) if !s.ReadBytes(&p.cv, 32) { return nil, errors.New("could not read cv") } - if !s.ReadBytes(&p.anchor, 32) { + if version <= 4 && !s.ReadBytes(&p.anchor, 32) { return nil, errors.New("could not read anchor") } @@ -120,11 +156,11 @@ func (p *spend) ParseFromSlice(data []byte) ([]byte, error) { return nil, errors.New("could not read rk") } - if !s.ReadBytes(&p.zkproof, 192) { + if version <= 4 && !s.ReadBytes(&p.zkproof, 192) { return nil, errors.New("could not read zkproof") } - if !s.ReadBytes(&p.spendAuthSig, 64) { + if version <= 4 && !s.ReadBytes(&p.spendAuthSig, 64) { return nil, errors.New("could not read spendAuthSig") } @@ -148,7 +184,7 @@ type output struct { zkproof []byte // 192 } -func (p *output) ParseFromSlice(data []byte) ([]byte, error) { +func (p *output) ParseFromSlice(data []byte, version uint32) ([]byte, error) { s := bytestring.String(data) if !s.ReadBytes(&p.cv, 32) { @@ -171,7 +207,7 @@ func (p *output) ParseFromSlice(data []byte) ([]byte, error) { return nil, errors.New("could not read outCiphertext") } - if !s.ReadBytes(&p.zkproof, 192) { + if version <= 4 && !s.ReadBytes(&p.zkproof, 192) { return nil, errors.New("could not read zkproof") } @@ -325,65 +361,14 @@ func (tx *Transaction) ToCompact(index int) *walletrpc.CompactTx { return ctx } -// ParseFromSlice deserializes a single transaction from the given data. -func (tx *Transaction) ParseFromSlice(data []byte) ([]byte, error) { +// parse versions 2 through 4 +func (tx *Transaction) parseV1to4(data []byte, version uint32) ([]byte, error) { s := bytestring.String(data) - - // declare here to prevent shadowing problems in cryptobyte assignments var err error - - var header uint32 - if !s.ReadUint32(&header) { - return nil, errors.New("could not read header") + s, err = tx.ParseTransparent([]byte(s)) + if err != nil { + return nil, err } - - tx.fOverwintered = (header >> 31) == 1 - tx.version = header & 0x7FFFFFFF - - if tx.version >= 3 { - if !s.ReadUint32(&tx.nVersionGroupID) { - return nil, errors.New("could not read nVersionGroupId") - } - } - - var txInCount int - if !s.ReadCompactSize(&txInCount) { - return nil, errors.New("could not read tx_in_count") - } - - // TODO: Duplicate/otherwise-too-many transactions are a possible DoS - // TODO: vector. At the moment we're assuming trusted input. - // See https://nvd.nist.gov/vuln/detail/CVE-2018-17144 for an example. - - if txInCount > 0 { - tx.transparentInputs = make([]*txIn, txInCount) - for i := 0; i < txInCount; i++ { - ti := &txIn{} - s, err = ti.ParseFromSlice([]byte(s)) - if err != nil { - return nil, errors.Wrap(err, "while parsing transparent input") - } - tx.transparentInputs[i] = ti - } - } - - var txOutCount int - if !s.ReadCompactSize(&txOutCount) { - return nil, errors.New("could not read tx_out_count") - } - - if txOutCount > 0 { - tx.transparentOutputs = make([]*txOut, txOutCount) - for i := 0; i < txOutCount; i++ { - to := &txOut{} - s, err = to.ParseFromSlice([]byte(s)) - if err != nil { - return nil, errors.Wrap(err, "while parsing transparent output") - } - tx.transparentOutputs[i] = to - } - } - if !s.ReadUint32(&tx.nLockTime) { return nil, errors.New("could not read nLockTime") } @@ -397,35 +382,31 @@ func (tx *Transaction) ParseFromSlice(data []byte) ([]byte, error) { var spendCount, outputCount int if tx.version >= 4 { - if !s.ReadInt64(&tx.valueBalance) { + if !s.ReadInt64(&tx.valueBalanceSapling) { return nil, errors.New("could not read valueBalance") } - if !s.ReadCompactSize(&spendCount) { return nil, errors.New("could not read nShieldedSpend") } - if spendCount > 0 { tx.shieldedSpends = make([]*spend, spendCount) for i := 0; i < spendCount; i++ { newSpend := &spend{} - s, err = newSpend.ParseFromSlice([]byte(s)) + s, err = newSpend.ParseFromSlice([]byte(s), tx.version) if err != nil { return nil, errors.Wrap(err, "while parsing shielded Spend") } tx.shieldedSpends[i] = newSpend } } - if !s.ReadCompactSize(&outputCount) { return nil, errors.New("could not read nShieldedOutput") } - if outputCount > 0 { tx.shieldedOutputs = make([]*output, outputCount) for i := 0; i < outputCount; i++ { newOutput := &output{} - s, err = newOutput.ParseFromSlice([]byte(s)) + s, err = newOutput.ParseFromSlice([]byte(s), tx.version) if err != nil { return nil, errors.Wrap(err, "while parsing shielded Output") } @@ -433,40 +414,175 @@ func (tx *Transaction) ParseFromSlice(data []byte) ([]byte, error) { } } } + var joinSplitCount int + if !s.ReadCompactSize(&joinSplitCount) { + return nil, errors.New("could not read nJoinSplit") + } - if tx.version >= 2 { - var joinSplitCount int - if !s.ReadCompactSize(&joinSplitCount) { - return nil, errors.New("could not read nJoinSplit") - } - - if joinSplitCount > 0 { - tx.joinSplits = make([]*joinSplit, joinSplitCount) - for i := 0; i < joinSplitCount; i++ { - js := &joinSplit{version: tx.version} - s, err = js.ParseFromSlice([]byte(s)) - if err != nil { - return nil, errors.Wrap(err, "while parsing JoinSplit") - } - tx.joinSplits[i] = js + if joinSplitCount > 0 { + tx.joinSplits = make([]*joinSplit, joinSplitCount) + for i := 0; i < joinSplitCount; i++ { + js := &joinSplit{version: tx.version} + s, err = js.ParseFromSlice([]byte(s)) + if err != nil { + return nil, errors.Wrap(err, "while parsing JoinSplit") } + tx.joinSplits[i] = js + } - if !s.ReadBytes(&tx.joinSplitPubKey, 32) { - return nil, errors.New("could not read joinSplitPubKey") - } + if !s.ReadBytes(&tx.joinSplitPubKey, 32) { + return nil, errors.New("could not read joinSplitPubKey") + } - if !s.ReadBytes(&tx.joinSplitSig, 64) { - return nil, errors.New("could not read joinSplitSig") - } + if !s.ReadBytes(&tx.joinSplitSig, 64) { + return nil, errors.New("could not read joinSplitSig") } } + if tx.version >= 4 { + if spendCount+outputCount > 0 && !s.ReadBytes(&tx.bindingSigSapling, 64) { + return nil, errors.New("could not read bindingSigSapling") + } + } + return s, nil +} - if tx.version >= 4 && (spendCount+outputCount > 0) { - if !s.ReadBytes(&tx.bindingSig, 64) { - return nil, errors.New("could not read bindingSig") +// parse version 5 +func (tx *Transaction) parseV5(data []byte, version uint32) ([]byte, error) { + s := bytestring.String(data) + var err error + if !s.ReadUint32(&tx.consensusBranchID) { + return nil, errors.New("could not read nVersionGroupId") + } + if !s.ReadUint32(&tx.nLockTime) { + return nil, errors.New("could not read nLockTime") + } + if !s.ReadUint32(&tx.nExpiryHeight) { + return nil, errors.New("could not read nExpiryHeight") + } + s, err = tx.ParseTransparent([]byte(s)) + if err != nil { + return nil, err + } + + var spendCount, outputCount int + if !s.ReadCompactSize(&spendCount) { + return nil, errors.New("could not read nShieldedSpend") + } + tx.shieldedSpends = make([]*spend, spendCount) + for i := 0; i < spendCount; i++ { + newSpend := &spend{} + s, err = newSpend.ParseFromSlice([]byte(s), tx.version) + if err != nil { + return nil, errors.Wrap(err, "while parsing shielded Spend") + } + tx.shieldedSpends[i] = newSpend + } + if !s.ReadCompactSize(&outputCount) { + return nil, errors.New("could not read nShieldedOutput") + } + tx.shieldedOutputs = make([]*output, outputCount) + for i := 0; i < outputCount; i++ { + newOutput := &output{} + s, err = newOutput.ParseFromSlice([]byte(s), tx.version) + if err != nil { + return nil, errors.Wrap(err, "while parsing shielded Output") + } + tx.shieldedOutputs[i] = newOutput + } + if spendCount+outputCount > 0 && !s.ReadInt64(&tx.valueBalanceSapling) { + return nil, errors.New("could not read valueBalance") + } + if spendCount > 0 && !s.Skip(32) { + return nil, errors.New("could not skip anchorSapling") + } + if !s.Skip(192 * spendCount) { + return nil, errors.New("could not skip vSpendProofsSapling") + } + if !s.Skip(64 * spendCount) { + return nil, errors.New("could not skip vSpendAuthSigsSapling") + } + if !s.Skip(192 * outputCount) { + return nil, errors.New("could not skip vOutputProofsSapling") + } + if spendCount+outputCount > 0 && !s.ReadBytes(&tx.bindingSigSapling, 64) { + return nil, errors.New("could not read bindingSigSapling") + } + var actionsCount int + if !s.ReadCompactSize(&actionsCount) { + return nil, errors.New("could not read nActionsOrchard") + } + if !s.Skip(820 * actionsCount) { + return nil, errors.New("could not skip vActionsOrchard") + } + if actionsCount > 0 { + if !s.Skip(1) { + return nil, errors.New("could not skip flagsOrchard") + } + if !s.Skip(8) { + return nil, errors.New("could not skip valueBalanceOrchard") + } + if !s.Skip(32) { + return nil, errors.New("could not skip anchorOrchard") + } + var proofsCount int + if !s.ReadCompactSize(&proofsCount) { + return nil, errors.New("could not read sizeProofsOrchard") + } + if !s.Skip(proofsCount) { + return nil, errors.New("could not skip proofsOrchard") + } + if !s.Skip(64 * actionsCount) { + return nil, errors.New("could not skip vSpendAuthSigsOrchard") + } + if !s.Skip(64) { + return nil, errors.New("could not skip bindingSigOrchard") } } + return s, nil +} +// ParseFromSlice deserializes a single transaction from the given data. +func (tx *Transaction) ParseFromSlice(data []byte) ([]byte, error) { + s := bytestring.String(data) + + // declare here to prevent shadowing problems in cryptobyte assignments + var err error + + var header uint32 + if !s.ReadUint32(&header) { + return nil, errors.New("could not read header") + } + + tx.fOverwintered = (header >> 31) == 1 + tx.version = header & 0x7FFFFFFF + + // Implement the effective version rule. From the spec section 7.1: + // + // "Version constraints apply to the effectiveVersion, which is equal to + // min(2, version) when fOverwintered = 0 and to version otherwise." + if !tx.fOverwintered && tx.version > 2 { + tx.version = 2 + } + + // Spec says fOverwinter must be set for version 5. + if tx.version >= 5 && !tx.fOverwintered { + return nil, errors.New(fmt.Sprintf("version %d requires fOverwinter", tx.version)) + } + + if tx.version >= 3 { + if !s.ReadUint32(&tx.nVersionGroupID) { + return nil, errors.New("could not read nVersionGroupId") + } + } + // parse the main part of the transaction + if tx.version <= 4 { + s, err = tx.parseV1to4([]byte(s), tx.version) + } else { + s, err = tx.parseV5([]byte(s), tx.version) + } + if err != nil { + return nil, err + } // TODO: implement rawBytes with MarshalBinary() instead txLen := len(data) - len(s) tx.rawBytes = data[:txLen] diff --git a/parser/transaction_test.go b/parser/transaction_test.go index 60671132..1d02ca6b 100644 --- a/parser/transaction_test.go +++ b/parser/transaction_test.go @@ -57,10 +57,10 @@ type txTestVector struct { joinSplitPubKey, joinSplitSig string // Sapling-only - valueBalance string // encoded int64 - spends []spendTestVector - outputs []outputTestVector - bindingSig string + valueBalanceSapling string // encoded int64 + spends []spendTestVector + outputs []outputTestVector + bindingSigSapling string } // https://github.com/zcash/zips/blob/master/zip-0143.rst @@ -520,9 +520,9 @@ var zip243tests = []txTestVector{ {"e7719811893e0000", "095200ac6551ac636565"}, {"b2835a0805750200", "025151"}, }, - nLockTime: "481cdd86", - nExpiryHeight: "b3cc4318", - valueBalance: "442117623ceb0500", + nLockTime: "481cdd86", + nExpiryHeight: "b3cc4318", + valueBalanceSapling: "442117623ceb0500", spends: []spendTestVector{ { cv: "1b3d1a027c2c40590958b7eb13d742a997738c46a458965baf276ba92f272c72", @@ -609,9 +609,9 @@ var zip243tests = []txTestVector{ }, }, }, - joinSplitPubKey: "f4599f5aa5dfbb45da60cdceab7eefde89be63f3f7c0d2324847cce1405def7c", - joinSplitSig: "469b0e272494e5df54f568656cb9c8818d92b72b8bc34db7bb3112487e746eefe4e808bbb287d99bf07d00dabededc5e5f074ffeae0cba7da3a516c173be1c51", - bindingSig: "3323e119f635e8209a074b216b7023fadc2d25949c90037e71e3e550726d210a2c688342e52440635e9cc14afe10102621a9c9accb782e9e4a5fa87f0a956f5b", + joinSplitPubKey: "f4599f5aa5dfbb45da60cdceab7eefde89be63f3f7c0d2324847cce1405def7c", + joinSplitSig: "469b0e272494e5df54f568656cb9c8818d92b72b8bc34db7bb3112487e746eefe4e808bbb287d99bf07d00dabededc5e5f074ffeae0cba7da3a516c173be1c51", + bindingSigSapling: "3323e119f635e8209a074b216b7023fadc2d25949c90037e71e3e550726d210a2c688342e52440635e9cc14afe10102621a9c9accb782e9e4a5fa87f0a956f5b", }, // Test vector 2 { @@ -626,9 +626,9 @@ var zip243tests = []txTestVector{ {"e91cb65a63b70100", "09516a6a656aac636565"}, {"5cc7c9aae5bd0300", "02636a"}, }, - nLockTime: "675cb83e", - nExpiryHeight: "43e29c17", - valueBalance: "44b8b5b99ce30500", + nLockTime: "675cb83e", + nExpiryHeight: "43e29c17", + valueBalanceSapling: "44b8b5b99ce30500", spends: []spendTestVector{ { cv: "b0f5b874a6ecabe6c56ee58b67d02f5d47db8cc3458435d5088d69b2240c28f3", @@ -656,11 +656,11 @@ var zip243tests = []txTestVector{ spendAuthSig: "9c8a091ffdc75b7ecfdc7c12995a5e37ce3488bd29f8629d68f696492448dd526697476dc061346ebe3f677217ff9c60efce943af28dfd3f9e59692598a6047c", }, }, - outputs: nil, - vJoinSplits: nil, - joinSplitPubKey: "", - joinSplitSig: "", - bindingSig: "c01400f1ab5730eac0ae8d5843d5051c376240172af218d7a1ecfe65b4f75100638983c14de4974755dade8018c9b8f4543fb095961513e67c61dbc59c607f9b", + outputs: nil, + vJoinSplits: nil, + joinSplitPubKey: "", + joinSplitSig: "", + bindingSigSapling: "c01400f1ab5730eac0ae8d5843d5051c376240172af218d7a1ecfe65b4f75100638983c14de4974755dade8018c9b8f4543fb095961513e67c61dbc59c607f9b", }, } @@ -747,10 +747,10 @@ func TestSaplingTransactionParser(t *testing.T) { } // Begin Sapling-specific tests - testValueBalanceBytes, _ := hex.DecodeString(tt.valueBalance) + testValueBalanceBytes, _ := hex.DecodeString(tt.valueBalanceSapling) testValueBalance := int64(binary.LittleEndian.Uint64(testValueBalanceBytes)) - if testValueBalance != tx.valueBalance { - t.Errorf("Test %d: valueBalance mismatch %d %d", i, testValueBalance, tx.valueBalance) + if testValueBalance != tx.valueBalanceSapling { + t.Errorf("Test %d: valueBalance mismatch %d %d", i, testValueBalance, tx.valueBalanceSapling) continue } @@ -762,9 +762,9 @@ func TestSaplingTransactionParser(t *testing.T) { continue } - testBinding, _ := hex.DecodeString(tt.bindingSig) - if !bytes.Equal(testBinding, tx.bindingSig) { - t.Errorf("Test %d: bindingSig %x %x", i, testBinding, tx.bindingSig) + testBinding, _ := hex.DecodeString(tt.bindingSigSapling) + if !bytes.Equal(testBinding, tx.bindingSigSapling) { + t.Errorf("Test %d: bindingSigSapling %x %x", i, testBinding, tx.bindingSigSapling) continue }