477 lines
12 KiB
Go
477 lines
12 KiB
Go
package parser
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/zcash-hackworks/lightwalletd/parser/internal/bytestring"
|
|
"github.com/zcash-hackworks/lightwalletd/walletrpc"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
// Txin format as described in https://en.bitcoin.it/wiki/Transaction
|
|
type txIn struct {
|
|
// SHA256d of a previous (to-be-used) transaction
|
|
PrevTxHash []byte
|
|
|
|
// Index of the to-be-used output in the previous tx
|
|
PrevTxOutIndex uint32
|
|
|
|
// CompactSize-prefixed, could be a pubkey or a script
|
|
ScriptSig []byte
|
|
|
|
// Bitcoin: "normally 0xFFFFFFFF; irrelevant unless transaction's lock_time > 0"
|
|
SequenceNumber uint32
|
|
}
|
|
|
|
func (tx *txIn) ParseFromSlice(data []byte) ([]byte, error) {
|
|
s := bytestring.String(data)
|
|
|
|
if ok := s.ReadBytes(&tx.PrevTxHash, 32); !ok {
|
|
return nil, errors.New("could not read PrevTxHash")
|
|
}
|
|
|
|
if ok := s.ReadUint32(&tx.PrevTxOutIndex); !ok {
|
|
return nil, errors.New("could not read PrevTxOutIndex")
|
|
}
|
|
|
|
if ok := s.ReadCompactLengthPrefixed((*bytestring.String)(&tx.ScriptSig)); !ok {
|
|
return nil, errors.New("could not read ScriptSig")
|
|
}
|
|
|
|
if ok := s.ReadUint32(&tx.SequenceNumber); !ok {
|
|
return nil, errors.New("could not read SequenceNumber")
|
|
}
|
|
|
|
return []byte(s), nil
|
|
}
|
|
|
|
// Txout format as described in https://en.bitcoin.it/wiki/Transaction
|
|
type txOut struct {
|
|
// Non-negative int giving the number of zatoshis to be transferred
|
|
Value uint64
|
|
|
|
// Script. CompactSize-prefixed.
|
|
Script []byte
|
|
}
|
|
|
|
func (tx *txOut) ParseFromSlice(data []byte) ([]byte, error) {
|
|
s := bytestring.String(data)
|
|
|
|
if ok := s.ReadUint64(&tx.Value); !ok {
|
|
return nil, errors.New("could not read txOut value")
|
|
}
|
|
|
|
if ok := s.ReadCompactLengthPrefixed((*bytestring.String)(&tx.Script)); !ok {
|
|
return nil, errors.New("could not read txOut script")
|
|
}
|
|
|
|
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.
|
|
type spend struct {
|
|
cv []byte // 32
|
|
anchor []byte // 32
|
|
nullifier []byte // 32
|
|
rk []byte // 32
|
|
zkproof []byte // 192
|
|
spendAuthSig []byte // 64
|
|
}
|
|
|
|
func (p *spend) ParseFromSlice(data []byte) ([]byte, error) {
|
|
s := bytestring.String(data)
|
|
|
|
if ok := s.ReadBytes(&p.cv, 32); !ok {
|
|
return nil, errors.New("could not read cv")
|
|
}
|
|
|
|
if ok := s.ReadBytes(&p.anchor, 32); !ok {
|
|
return nil, errors.New("could not read anchor")
|
|
}
|
|
|
|
if ok := s.ReadBytes(&p.nullifier, 32); !ok {
|
|
return nil, errors.New("could not read nullifier")
|
|
}
|
|
|
|
if ok := s.ReadBytes(&p.rk, 32); !ok {
|
|
return nil, errors.New("could not read rk")
|
|
}
|
|
|
|
if ok := s.ReadBytes(&p.zkproof, 192); !ok {
|
|
return nil, errors.New("could not read zkproof")
|
|
}
|
|
|
|
if ok := s.ReadBytes(&p.spendAuthSig, 64); !ok {
|
|
return nil, errors.New("could not read spendAuthSig")
|
|
}
|
|
|
|
return []byte(s), nil
|
|
}
|
|
|
|
func (p *spend) ToCompact() *walletrpc.CompactSpend {
|
|
return &walletrpc.CompactSpend{
|
|
Nf: p.nullifier,
|
|
}
|
|
}
|
|
|
|
// output is a Sapling Output Description as described in section 7.4 of the
|
|
// Zcash protocol spec. Total size is 948.
|
|
type output struct {
|
|
cv []byte // 32
|
|
cmu []byte // 32
|
|
ephemeralKey []byte // 32
|
|
encCiphertext []byte // 580
|
|
outCiphertext []byte // 80
|
|
zkproof []byte // 192
|
|
}
|
|
|
|
func (p *output) ParseFromSlice(data []byte) ([]byte, error) {
|
|
s := bytestring.String(data)
|
|
|
|
if ok := s.ReadBytes(&p.cv, 32); !ok {
|
|
return nil, errors.New("could not read cv")
|
|
}
|
|
|
|
if ok := s.ReadBytes(&p.cmu, 32); !ok {
|
|
return nil, errors.New("could not read cmu")
|
|
}
|
|
|
|
if ok := s.ReadBytes(&p.ephemeralKey, 32); !ok {
|
|
return nil, errors.New("could not read ephemeralKey")
|
|
}
|
|
|
|
if ok := s.ReadBytes(&p.encCiphertext, 580); !ok {
|
|
return nil, errors.New("could not read encCiphertext")
|
|
}
|
|
|
|
if ok := s.ReadBytes(&p.outCiphertext, 80); !ok {
|
|
return nil, errors.New("could not read outCiphertext")
|
|
}
|
|
|
|
if ok := s.ReadBytes(&p.zkproof, 192); !ok {
|
|
return nil, errors.New("could not read zkproof")
|
|
}
|
|
|
|
return []byte(s), nil
|
|
}
|
|
|
|
func (p *output) ToCompact() *walletrpc.CompactOutput {
|
|
return &walletrpc.CompactOutput{
|
|
Cmu: p.cmu,
|
|
Epk: p.ephemeralKey,
|
|
Ciphertext: p.encCiphertext[:52],
|
|
}
|
|
}
|
|
|
|
// joinSplit is a JoinSplit description as described in 7.2 of the Zcash
|
|
// protocol spec. Its exact contents differ by transaction version and network
|
|
// upgrade level.
|
|
type joinSplit struct {
|
|
vpubOld uint64
|
|
vpubNew uint64
|
|
anchor []byte // 32
|
|
nullifiers [2][]byte // 64 [N_old][32]byte
|
|
commitments [2][]byte // 64 [N_new][32]byte
|
|
ephemeralKey []byte // 32
|
|
randomSeed []byte // 32
|
|
vmacs [2][]byte // 64 [N_old][32]byte
|
|
proofPHGR13 []byte // 296
|
|
proofGroth16 []byte // 192
|
|
encCiphertexts [2][]byte // 1202 [N_new][601]byte
|
|
|
|
// not actually in the format, but needed for parsing
|
|
version uint32
|
|
}
|
|
|
|
func (p *joinSplit) ParseFromSlice(data []byte) ([]byte, error) {
|
|
s := bytestring.String(data)
|
|
|
|
if ok := s.ReadUint64(&p.vpubOld); !ok {
|
|
return nil, errors.New("could not read vpubOld")
|
|
}
|
|
|
|
if ok := s.ReadUint64(&p.vpubNew); !ok {
|
|
return nil, errors.New("could not read vpubNew")
|
|
}
|
|
|
|
if ok := s.ReadBytes(&p.anchor, 32); !ok {
|
|
return nil, errors.New("could not read anchor")
|
|
}
|
|
|
|
for i := 0; i < 2; i++ {
|
|
if ok := s.ReadBytes(&p.nullifiers[i], 32); !ok {
|
|
return nil, errors.New("could not read a nullifier")
|
|
}
|
|
}
|
|
|
|
for i := 0; i < 2; i++ {
|
|
if ok := s.ReadBytes(&p.commitments[i], 32); !ok {
|
|
return nil, errors.New("could not read a commitment")
|
|
}
|
|
}
|
|
|
|
if ok := s.ReadBytes(&p.ephemeralKey, 32); !ok {
|
|
return nil, errors.New("could not read ephemeralKey")
|
|
}
|
|
|
|
if ok := s.ReadBytes(&p.randomSeed, 32); !ok {
|
|
return nil, errors.New("could not read randomSeed")
|
|
}
|
|
|
|
for i := 0; i < 2; i++ {
|
|
if ok := s.ReadBytes(&p.vmacs[i], 32); !ok {
|
|
return nil, errors.New("could not read a vmac")
|
|
}
|
|
}
|
|
|
|
if p.version == 2 || p.version == 3 {
|
|
if ok := s.ReadBytes(&p.proofPHGR13, 296); !ok {
|
|
return nil, errors.New("could not read PHGR13 proof")
|
|
}
|
|
} else if p.version >= 4 {
|
|
if ok := s.ReadBytes(&p.proofGroth16, 192); !ok {
|
|
return nil, errors.New("could not read Groth16 proof")
|
|
}
|
|
} else {
|
|
return nil, errors.New("unexpected transaction version")
|
|
}
|
|
|
|
for i := 0; i < 2; i++ {
|
|
if ok := s.ReadBytes(&p.encCiphertexts[i], 601); !ok {
|
|
return nil, errors.New("could not read an encCiphertext")
|
|
}
|
|
}
|
|
|
|
return []byte(s), nil
|
|
}
|
|
|
|
type Transaction struct {
|
|
*rawTransaction
|
|
rawBytes []byte
|
|
txId []byte
|
|
}
|
|
|
|
// GetDisplayHash returns the transaction hash in big-endian display order.
|
|
func (tx *Transaction) GetDisplayHash() []byte {
|
|
if tx.txId != nil {
|
|
return tx.txId
|
|
}
|
|
|
|
// SHA256d
|
|
digest := sha256.Sum256(tx.rawBytes)
|
|
digest = sha256.Sum256(digest[:])
|
|
|
|
// Reverse byte order
|
|
for i := 0; i < len(digest)/2; i++ {
|
|
j := len(digest) - 1 - i
|
|
digest[i], digest[j] = digest[j], digest[i]
|
|
}
|
|
|
|
tx.txId = digest[:]
|
|
return tx.txId
|
|
}
|
|
|
|
// GetEncodableHash returns the transaction hash in little-endian wire format order.
|
|
func (tx *Transaction) GetEncodableHash() []byte {
|
|
digest := sha256.Sum256(tx.rawBytes)
|
|
digest = sha256.Sum256(digest[:])
|
|
return digest[:]
|
|
}
|
|
|
|
func (tx *Transaction) Bytes() []byte {
|
|
return tx.rawBytes
|
|
}
|
|
|
|
func (tx *Transaction) HasSaplingTransactions() bool {
|
|
return tx.version >= 4 && (len(tx.shieldedSpends)+len(tx.shieldedOutputs)) > 0
|
|
}
|
|
|
|
func (tx *Transaction) ToCompact(index int) *walletrpc.CompactTx {
|
|
ctx := &walletrpc.CompactTx{
|
|
Index: uint64(index), // index is contextual
|
|
Hash: tx.GetEncodableHash(),
|
|
//Fee: 0, // TODO: calculate fees
|
|
Spends: make([]*walletrpc.CompactSpend, len(tx.shieldedSpends)),
|
|
Outputs: make([]*walletrpc.CompactOutput, len(tx.shieldedOutputs)),
|
|
}
|
|
for i, spend := range tx.shieldedSpends {
|
|
ctx.Spends[i] = spend.ToCompact()
|
|
}
|
|
for i, output := range tx.shieldedOutputs {
|
|
ctx.Outputs[i] = output.ToCompact()
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
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 ok := s.ReadUint32(&header); !ok {
|
|
return nil, errors.New("could not read header")
|
|
}
|
|
|
|
tx.fOverwintered = (header >> 31) == 1
|
|
tx.version = header & 0x7FFFFFFF
|
|
|
|
if tx.version >= 3 {
|
|
if ok := s.ReadUint32(&tx.nVersionGroupId); !ok {
|
|
return nil, errors.New("could not read nVersionGroupId")
|
|
}
|
|
}
|
|
|
|
var txInCount int
|
|
if ok := s.ReadCompactSize(&txInCount); !ok {
|
|
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 ok := s.ReadCompactSize(&txOutCount); !ok {
|
|
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 ok := s.ReadUint32(&tx.nLockTime); !ok {
|
|
return nil, errors.New("could not read nLockTime")
|
|
}
|
|
|
|
if tx.fOverwintered {
|
|
if ok := s.ReadUint32(&tx.nExpiryHeight); !ok {
|
|
return nil, errors.New("could not read nExpiryHeight")
|
|
}
|
|
}
|
|
|
|
var spendCount, outputCount int
|
|
|
|
if tx.version >= 4 {
|
|
if ok := s.ReadInt64(&tx.valueBalance); !ok {
|
|
return nil, errors.New("could not read valueBalance")
|
|
}
|
|
|
|
if ok := s.ReadCompactSize(&spendCount); !ok {
|
|
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))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "while parsing shielded Spend")
|
|
}
|
|
tx.shieldedSpends[i] = newSpend
|
|
}
|
|
}
|
|
|
|
if ok := s.ReadCompactSize(&outputCount); !ok {
|
|
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))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "while parsing shielded Output")
|
|
}
|
|
tx.shieldedOutputs[i] = newOutput
|
|
}
|
|
}
|
|
}
|
|
|
|
if tx.version >= 2 {
|
|
var joinSplitCount int
|
|
if ok := s.ReadCompactSize(&joinSplitCount); !ok {
|
|
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 ok := s.ReadBytes(&tx.joinSplitPubKey, 32); !ok {
|
|
return nil, errors.New("could not read joinSplitPubKey")
|
|
}
|
|
|
|
if ok := s.ReadBytes(&tx.joinSplitSig, 64); !ok {
|
|
return nil, errors.New("could not read joinSplitSig")
|
|
}
|
|
}
|
|
}
|
|
|
|
if tx.version >= 4 && (spendCount+outputCount > 0) {
|
|
if ok := s.ReadBytes(&tx.bindingSig, 64); !ok {
|
|
return nil, errors.New("could not read bindingSig")
|
|
}
|
|
}
|
|
|
|
// TODO: implement rawBytes with MarshalBinary() instead
|
|
txLen := len(data) - len(s)
|
|
tx.rawBytes = data[:txLen]
|
|
|
|
return []byte(s), nil
|
|
}
|
|
|
|
func NewTransaction() *Transaction {
|
|
return &Transaction{
|
|
rawTransaction: new(rawTransaction),
|
|
}
|
|
}
|