From 30996ab018b2536732c7b86aced742acc1484bed Mon Sep 17 00:00:00 2001 From: Jimmy Song Date: Thu, 27 Apr 2017 16:21:44 -0700 Subject: [PATCH] Correctly parses early coinbase tx's --- transaction.go | 304 ++++++++++++++++++++++++++++++++++++++++++-- transaction_test.go | 64 ++++++++++ varint.go | 19 ++- varint_test.go | 16 ++- 4 files changed, 381 insertions(+), 22 deletions(-) create mode 100644 transaction_test.go diff --git a/transaction.go b/transaction.go index 6945a20..f800197 100644 --- a/transaction.go +++ b/transaction.go @@ -1,10 +1,29 @@ package moneroutil -type Hash [32]byte -type PubKey [32]byte +import ( + "bytes" + "errors" + "fmt" +) + +const ( + txInGenMarker = 0xff + txInToScriptMarker = 0 + txInToScriptHashMarker = 1 + txInToKeyMarker = 2 + txOutToScriptMarker = 0 + txOutToScriptHashMarker = 1 + txOutToKeyMarker = 2 + + HashLength = 32 + PubKeyLength = 32 +) + +type Hash [HashLength]byte +type PubKey [PubKeyLength]byte type Signature struct { - c []byte - r []byte + c [PubKeyLength]byte + r [PubKeyLength]byte } type txOutToScript struct { @@ -17,11 +36,12 @@ type txOutToScriptHash struct { } type txOutToKey struct { - pubkey PubKey + key PubKey } type TxOutTargeter interface { TxOutTarget() []byte + String() string } type txInGen struct { @@ -30,15 +50,15 @@ type txInGen struct { type txInToScript struct { prev []byte - prevout uint64 + prevOut uint64 sigSet []byte } type txInToScriptHash struct { - prev Hash - prevout uint64 - txOutToScript []byte - sigSet []byte + prev Hash + prevOut uint64 + script []byte + sigSet []byte } type txInToKey struct { @@ -52,15 +72,15 @@ type TxInMaker interface { } type TxOut struct { - amount uint64 - targets TxOutTargeter + amount uint64 + target TxOutTargeter } type TransactionPrefix struct { version uint32 unlockTime uint64 vin []TxInMaker - vout TxOut + vout []*TxOut extra []byte } @@ -71,3 +91,261 @@ type Transaction struct { hash []byte blobSize uint32 } + +func (h *Hash) Serialize() (result []byte) { + result = h[:] + return +} + +func (p *PubKey) Serialize() (result []byte) { + result = p[:] + return +} + +func (s *Signature) Serialize() (result []byte) { + result = make([]byte, 64) + copy(result, s.c[:]) + copy(result[32:64], s.r[:]) + return +} + +func (t *txOutToScript) TxOutTarget() (result []byte) { + result = []byte{txInToScriptMarker} + for i, pubkey := range t.pubKeys { + if i != 0 { + result = append(result, byte(txOutToScriptMarker)) + } + result = append(result, pubkey.Serialize()...) + } + result = append(result, t.script...) + return +} + +func (t *txOutToScript) String() (result string) { + result = fmt.Sprintf("script: %x", t.script) + return +} + +func (t *txOutToScriptHash) TxOutTarget() (result []byte) { + result = append([]byte{txInToScriptHashMarker}, t.hash.Serialize()...) + return +} + +func (t *txOutToScriptHash) String() (result string) { + result = fmt.Sprintf("hash: %x", t.hash) + return +} + +func (t *txOutToKey) TxOutTarget() (result []byte) { + result = append([]byte{txInToKeyMarker}, t.key.Serialize()...) + return +} + +func (t *txOutToKey) String() (result string) { + result = fmt.Sprintf("key: %x", t.key) + return +} + +func (t *txInGen) TxIn() (result []byte) { + result = append([]byte{txInGenMarker}, Uint64ToBytes(t.height)...) + return +} + +func (t *txInToScript) TxIn() (result []byte) { + result = append([]byte{txInToScriptMarker}, t.prev...) + result = append(result, Uint64ToBytes(t.prevOut)...) + result = append(result, t.sigSet...) + return +} + +func (t *txInToScriptHash) TxIn() (result []byte) { + result = append([]byte{txInToScriptHashMarker}, t.prev.Serialize()...) + result = append(result, Uint64ToBytes(t.prevOut)...) + result = append(result, t.script...) + result = append(result, t.sigSet...) + return +} + +func (t *txInToKey) TxIn() (result []byte) { + result = append([]byte{txInToKeyMarker}, Uint64ToBytes(t.amount)...) + for _, keyOffset := range t.keyOffsets { + result = append(result, Uint64ToBytes(keyOffset)...) + } + result = append(result, t.keyImage[:]...) + return +} + +func (t *TxOut) Serialize() (result []byte) { + result = append(Uint64ToBytes(t.amount), t.target.TxOutTarget()...) + return +} + +func (t *TransactionPrefix) Serialize() (result []byte) { + result = append(Uint64ToBytes(uint64(t.version)), Uint64ToBytes(t.unlockTime)...) + result = append(result, Uint64ToBytes(uint64(len(t.vin)))...) + for _, txIn := range t.vin { + result = append(result, txIn.TxIn()...) + } + result = append(result, Uint64ToBytes(uint64(len(t.vout)))...) + for _, txOut := range t.vout { + result = append(result, txOut.Serialize()...) + } + result = append(result, Uint64ToBytes(uint64(len(t.extra)))...) + result = append(result, t.extra...) + return +} + +func ParseTxInGen(buf *bytes.Buffer) (txIn *txInGen, err error) { + t := new(txInGen) + t.height, err = ReadVarInt(buf) + if err != nil { + return + } + txIn = t + return +} + +func ParseTxInToScript(buf *bytes.Buffer) (txIn *txInToScript, err error) { + err = errors.New("Unimplemented") + return +} + +func ParseTxInToScriptHash(buf *bytes.Buffer) (txIn *txInToScriptHash, err error) { + err = errors.New("Unimplemented") + return +} + +func ParseTxInToKey(buf *bytes.Buffer) (txIn *txInToKey, err error) { + err = errors.New("Unimplemented") + return +} + +func ParseTxIn(buf *bytes.Buffer) (txIn TxInMaker, err error) { + marker, err := buf.ReadByte() + if err != nil { + return + } + switch { + case marker == txInGenMarker: + txIn, err = ParseTxInGen(buf) + case marker == txInToScriptMarker: + txIn, err = ParseTxInToScript(buf) + case marker == txInToScriptHashMarker: + txIn, err = ParseTxInToScriptHash(buf) + case marker == txInToKeyMarker: + txIn, err = ParseTxInToKey(buf) + } + return +} + +func ParseTxOutToScript(buf *bytes.Buffer) (txOutTarget *txOutToScript, err error) { + err = errors.New("Unimplemented") + return +} + +func ParseTxOutToScriptHash(buf *bytes.Buffer) (txOutTarget *txOutToScriptHash, err error) { + err = errors.New("Unimplemented") + return +} + +func ParseTxOutToKey(buf *bytes.Buffer) (txOutTarget *txOutToKey, err error) { + t := new(txOutToKey) + pubKey := buf.Next(PubKeyLength) + if len(pubKey) != PubKeyLength { + err = errors.New("Buffer not long enough for public key") + return + } + copy(t.key[:], pubKey) + txOutTarget = t + return +} + +func ParseTxOut(buf *bytes.Buffer) (txOut *TxOut, err error) { + t := new(TxOut) + t.amount, err = ReadVarInt(buf) + if err != nil { + return + } + marker, err := buf.ReadByte() + if err != nil { + return + } + switch { + case marker == txOutToScriptMarker: + t.target, err = ParseTxOutToScript(buf) + case marker == txOutToScriptHashMarker: + t.target, err = ParseTxOutToScriptHash(buf) + case marker == txOutToKeyMarker: + t.target, err = ParseTxOutToKey(buf) + default: + err = errors.New("Bad Marker") + return + } + if err != nil { + return + } + txOut = t + return +} + +func ParseExtra(buf *bytes.Buffer) (extra []byte, err error) { + length, err := ReadVarInt(buf) + if err != nil { + return + } + e := buf.Next(int(length)) + if len(e) != int(length) { + err = errors.New("Not enough bytes for extra") + return + } + extra = e + return +} + +func ParseTransaction(buf *bytes.Buffer) (transaction *TransactionPrefix, err error) { + t := new(TransactionPrefix) + version, err := ReadVarInt(buf) + if err != nil { + return + } + t.version = uint32(version) + t.unlockTime, err = ReadVarInt(buf) + if err != nil { + return + } + numInputs, err := ReadVarInt(buf) + if err != nil { + return + } + t.vin = make([]TxInMaker, int(numInputs), int(numInputs)) + for i := 0; i < int(numInputs); i++ { + t.vin[i], err = ParseTxIn(buf) + if err != nil { + return + } + } + numOutputs, err := ReadVarInt(buf) + if err != nil { + return + } + t.vout = make([]*TxOut, int(numOutputs), int(numOutputs)) + for i := 0; i < int(numOutputs); i++ { + t.vout[i], err = ParseTxOut(buf) + if err != nil { + return + } + } + t.extra, err = ParseExtra(buf) + if err != nil { + return + } + transaction = t + return +} + +func (t *TransactionPrefix) OutputSum() (sum uint64) { + for _, output := range t.vout { + sum += output.amount + } + return +} diff --git a/transaction_test.go b/transaction_test.go new file mode 100644 index 0000000..842181e --- /dev/null +++ b/transaction_test.go @@ -0,0 +1,64 @@ +package moneroutil + +import ( + "bytes" + "encoding/hex" + "testing" +) + +func TestTransactionParse(t *testing.T) { + + serializedTxHex := "014b01ff0f098fd61702a3ef0df2a51b14891676b39f18d6c0b8c52bfc3f6ba5f63f792e993f73b900df8092f40102204e4d5992876b76cc8a30760c8fd82dbaf948cb2d858278a08f4d8db326682a8087a70e027a2328c2d86a1f84335010d183d0eef067195da4a57169ac9d1c8c1f84ec74a580d293ad03023c6411e620104487402583db89dd15245448092f339a30a4982e076c7ba0a7578094ebdc0302861696af6e92cea973fa217f3b4df3e8c43a2a1cb888b515e23d2d5d3d41ba688088aca3cf0202d3087348abe2dffa189c79f82e032f14cbd12cfd6ece0a5192ec235a4cf2c96e8090cad2c60e02feb619b90856473e906295252cc9a87abbb7f5db409579d3169be5ee23b35ab680e08d84ddcb010275cd0dbe02a00558669ef424c2d5f8cbefc8c4af711f1afde0404628c401b13580c0caf384a30202b1bb2e13bf7c1d88885b7ebf2be56e5064af24d69f3c92d9264550122eed1a0b2101bbac13803d9b7941444cc817292b91a9634fa6cee88ced917571df2e0c87ad79" + expectedHashHex := "e3a799da24d9f41aac231ba2efb853ae649283feaf5e1ba46b5fc2c194414c5d" + serializedTx, _ := hex.DecodeString(serializedTxHex) + expectedHash, _ := hex.DecodeString(expectedHashHex) + hash := Keccak256(serializedTx) + if bytes.Compare(expectedHash, hash) != 0 { + t.Fatalf("want %x, got %x", expectedHash, hash) + } + buffer := new(bytes.Buffer) + buffer.Write(serializedTx) + transaction, err := ParseTransaction(buffer) + if err != nil { + t.Fatalf("error parsing tx: %s", err) + } + wantVersion := uint32(1) + if wantVersion != transaction.version { + t.Fatalf("version: want %d, got %d", wantVersion, transaction.version) + } + wantUnlock := uint64(75) + if wantUnlock != transaction.unlockTime { + t.Fatalf("unlock: want %d, got %d", wantUnlock, transaction.unlockTime) + } + wantSum := uint64(17591934387855) + gotSum := transaction.OutputSum() + if wantSum != gotSum { + t.Fatalf("sum: want %d, got %d", wantSum, gotSum) + } + wantLen := 1 + gotLen := len(transaction.vin) + if wantLen != gotLen { + t.Fatalf("input len: want %d, got %d", wantLen, gotLen) + } + wantIn := []byte{0xff, 0x0f} + gotIn := transaction.vin[0].TxIn() + if bytes.Compare(wantIn, gotIn) != 0 { + t.Fatalf("input 1: want %x, got %x", wantIn, gotIn) + } + wantLen = 9 + gotLen = len(transaction.vout) + if wantLen != gotLen { + t.Fatalf("output len: want %d, got %d", wantLen, gotLen) + } + wantOutHex := "02a3ef0df2a51b14891676b39f18d6c0b8c52bfc3f6ba5f63f792e993f73b900df" + wantOut, _ := hex.DecodeString(wantOutHex) + gotOut := transaction.vout[0].target.TxOutTarget() + if bytes.Compare(wantOut, gotOut) != 0 { + t.Fatalf("input 1: want %x, got %x", wantOut, gotOut) + } + gotSerialized := transaction.Serialize() + if bytes.Compare(serializedTx, gotSerialized) != 0 { + t.Fatalf("serialized: want %x, got %x", serializedTx, gotSerialized) + } + +} diff --git a/varint.go b/varint.go index a77bfe8..2386b18 100644 --- a/varint.go +++ b/varint.go @@ -1,16 +1,25 @@ package moneroutil -func ReadVarInt(varInt []byte) (result int64) { - for i, b := range varInt { - result += (int64(b) & 0x7f) << uint(i*7) - if int64(b)&0x80 == 0 { +import ( + "bytes" +) + +func ReadVarInt(buf *bytes.Buffer) (result uint64, err error) { + var b byte + for i := 0; ; i++ { + b, err = buf.ReadByte() + if err != nil { + return + } + result += (uint64(b) & 0x7f) << uint(i*7) + if uint64(b)&0x80 == 0 { break } } return } -func WriteVarInt(num int64) (result []byte) { +func Uint64ToBytes(num uint64) (result []byte) { for ; num >= 0x80; num >>= 7 { result = append(result, byte((num&0x7f)|0x80)) } diff --git a/varint_test.go b/varint_test.go index cc465ba..f5e0418 100644 --- a/varint_test.go +++ b/varint_test.go @@ -9,7 +9,7 @@ func TestVarInt(t *testing.T) { tests := []struct { name string varInt []byte - want int64 + want uint64 }{ { name: "1 byte", @@ -32,15 +32,23 @@ func TestVarInt(t *testing.T) { want: 10000000000000, }, } - var got int64 + var got uint64 + var err error var gotVarInt []byte + buf := new(bytes.Buffer) for _, test := range tests { - gotVarInt = WriteVarInt(test.want) + gotVarInt = Uint64ToBytes(test.want) if bytes.Compare(gotVarInt, test.varInt) != 0 { t.Errorf("%s: varint want %x, got %x", test.name, test.varInt, gotVarInt) continue } - got = ReadVarInt(test.varInt) + buf.Reset() + buf.Write(test.varInt) + got, err = ReadVarInt(buf) + if err != nil { + t.Errorf("%s: %s", test.name, err) + continue + } if test.want != got { t.Errorf("%s: want %d, got %d", test.name, test.want, got) continue