package token import ( "bytes" "crypto/sha512" "encoding/binary" "encoding/hex" "fmt" "math/big" "slices" "github.com/iden3/go-iden3-crypto/poseidon" "github.com/pkg/errors" "golang.org/x/crypto/sha3" hgstate "source.quilibrium.com/quilibrium/monorepo/node/execution/state/hypergraph" "source.quilibrium.com/quilibrium/monorepo/types/crypto" "source.quilibrium.com/quilibrium/monorepo/types/execution/intrinsics" "source.quilibrium.com/quilibrium/monorepo/types/execution/state" "source.quilibrium.com/quilibrium/monorepo/types/hypergraph" "source.quilibrium.com/quilibrium/monorepo/types/keys" "source.quilibrium.com/quilibrium/monorepo/types/schema" qcrypto "source.quilibrium.com/quilibrium/monorepo/types/tries" ) const FRAME_2_1_CUTOVER = 244200 const FRAME_2_1_EXTENDED_ENROLL_END = 255840 const FRAME_2_1_EXTENDED_ENROLL_CONFIRM_END = FRAME_2_1_EXTENDED_ENROLL_END + 6500 // used to skip frame-based checks, for tests var BEHAVIOR_PASS = false // using ed448 derivation process of seed = [57]byte{0x00..} var publicReadKey, _ = hex.DecodeString("2cf07ca8d9ab1a4bb0902e25a9b90759dd54d881f54d52a76a17e79bf0361c325650f12746e4337ffb5940e7665ad7bf83f44af98d964bbe") // TransactionInput is an input specific to the Transaction flow, where a token // intrinsic is either configured as Acceptable, and the input is thus a // pending:PendingTransaction, or not Acceptable, and the input is thus a // coin:Coin. type TransactionInput struct { // Public input values: // The constructed commitment to the input balance. Commitment []byte // The underlying signature authorizing spend and proving validity. Signature []byte // The proofs of various attributes of the token. Must verify against the // transaction's multiproofs for full end-to-end verification. Proofs [][]byte // Private input values used for construction of public values: // The address of the input value address []byte // The underlying input value value *big.Int // The signing operation which sets the signature, after the outputs are // generated. signOp func(transcript []byte) error } func NewTransactionInput(address []byte) (*TransactionInput, error) { return &TransactionInput{ address: address, // buildutils:allow-slice-alias slice is static }, nil } func (i *TransactionInput) Prove(tx *Transaction, index int) ([]byte, error) { _, err := tx.hypergraph.GetVertex([64]byte(i.address)) if err != nil { return nil, errors.Wrap(err, "prove input") } tree, err := tx.hypergraph.GetVertexData([64]byte(i.address)) if err != nil { return nil, errors.Wrap(err, "prove input") } fnData, err := tree.Get([]byte{0}) if err != nil { return nil, errors.Wrap(err, "prove input") } frameNumber := uint64(0) if len(fnData) != 8 { return nil, errors.Wrap(errors.New("invalid frame number"), "prove input") } else { frameNumber = binary.BigEndian.Uint64(fnData[:8]) } var blind []byte if bytes.Equal(i.address[:32], QUIL_TOKEN_ADDRESS) && frameNumber <= FRAME_2_1_EXTENDED_ENROLL_CONFIRM_END && !BEHAVIOR_PASS { return nil, errors.Wrap(errors.New("invalid action"), "prove input") } coinTypeBI, err := poseidon.HashBytes( slices.Concat(i.address[:32], []byte("coin:Coin")), ) if err != nil { return nil, errors.Wrap(err, "prove input") } coinTypeBytes := coinTypeBI.FillBytes(make([]byte, 32)) pendingTypeBI, err := poseidon.HashBytes( slices.Concat(i.address[:32], []byte("pending:PendingTransaction")), ) if err != nil { return nil, errors.Wrap(err, "prove input") } pendingTypeBytes := pendingTypeBI.FillBytes(make([]byte, 32)) checkType, err := tree.Get(bytes.Repeat([]byte{0xff}, 32)) if err != nil { return nil, errors.Wrap(err, "prove input") } commitmentIndex := 1 << 2 oneTimeKeyIndex := 2 << 2 keyImageIndex := 3 << 2 coinBalanceIndex := 4 << 2 blindIndex := 5 << 2 addRef1Index := 6 << 2 addRef2Index := 7 << 2 if !bytes.Equal(coinTypeBytes, checkType) && tx.config.Behavior&Acceptable == 0 { return nil, errors.Wrap( errors.New("invalid type for address"), "prove input", ) } if tx.config.Behavior&Acceptable != 0 { if !bytes.Equal(pendingTypeBytes, checkType) { return nil, errors.Wrap( errors.New( fmt.Sprintf( "invalid type for address: %x, expected %x", checkType, pendingTypeBytes, ), ), "prove input", ) } commitmentIndex = 1 << 2 oneTimeKeyIndex = 2 << 2 keyImageIndex = 4 << 2 coinBalanceIndex = 6 << 2 blindIndex = 8 << 2 addRef1Index = 10 << 2 addRef2Index = 11 << 2 // R oneTimeKey, err := tree.Get([]byte{byte(oneTimeKeyIndex)}) if err != nil { return nil, errors.Wrap(err, "prove input") } // VK possibleViewKey, err := tx.keyRing.GetAgreementKey( "q-view-key", i.address, crypto.KeyTypeDecaf448, ) if err != nil { return nil, errors.Wrap(err, "prove input") } viewKey, ok := possibleViewKey.(crypto.DecafAgreement) if !ok { return nil, errors.Wrap(errors.New("invalid view key"), "prove input") } // SK possibleSpendKey, err := tx.keyRing.GetAgreementKey( "q-spend-key", i.address, crypto.KeyTypeDecaf448, ) if err != nil { return nil, errors.Wrap(err, "prove input") } spendKey, ok := possibleSpendKey.(crypto.DecafAgreement) if !ok { return nil, errors.Wrap(errors.New("invalid spend key"), "prove input") } // rVK shared, err := viewKey.AgreeWithAndHashToScalar(oneTimeKey) if err != nil { return nil, errors.Wrap(err, "prove input") } checkKeyImage, err := shared.Add(spendKey.Public()) if err != nil { return nil, errors.Wrap(err, "prove input") } keyImage, err := tree.Get([]byte{byte(keyImageIndex)}) if err != nil { return nil, errors.Wrap(err, "prove input") } if !bytes.Equal(checkKeyImage, keyImage) { oneTimeKeyIndex = 3 << 2 keyImageIndex = 5 << 2 coinBalanceIndex = 7 << 2 blindIndex = 9 << 2 addRef1Index = 12 << 2 addRef2Index = 13 << 2 } } // C commitment, err := tree.Get([]byte{byte(commitmentIndex)}) if err != nil { return nil, errors.Wrap(err, "prove input") } // R oneTimeKey, err := tree.Get([]byte{byte(oneTimeKeyIndex)}) if err != nil { return nil, errors.Wrap(err, "prove input") } // VK possibleViewKey, err := tx.keyRing.GetAgreementKey( "q-view-key", i.address, crypto.KeyTypeDecaf448, ) if err != nil { return nil, errors.Wrap(err, "prove input") } viewKey, ok := possibleViewKey.(crypto.DecafAgreement) if !ok { return nil, errors.Wrap(errors.New("invalid view key"), "prove input") } // SK possibleSpendKey, err := tx.keyRing.GetAgreementKey( "q-spend-key", i.address, crypto.KeyTypeDecaf448, ) if err != nil { return nil, errors.Wrap(err, "prove input") } spendKey, ok := possibleSpendKey.(crypto.DecafAgreement) if !ok { return nil, errors.Wrap(errors.New("invalid spend key"), "prove input") } // rVK shared, err := viewKey.AgreeWithAndHashToScalar(oneTimeKey) if err != nil { return nil, errors.Wrap(err, "prove input") } coinBalanceData, err := tree.Get([]byte{byte(coinBalanceIndex)}) if err != nil { return nil, errors.Wrap(err, "prove input") } blindData, err := tree.Get([]byte{byte(blindIndex)}) if err != nil { return nil, errors.Wrap(err, "prove input") } blindMask := make([]byte, 56) coinMask := make([]byte, 56) shake := sha3.NewCShake256([]byte{}, []byte("blind")) shake.Write(shared.Public()) shake.Read(blindMask) shake = sha3.NewCShake256([]byte{}, []byte("coin")) shake.Write(shared.Public()) shake.Read(coinMask) for i := range blindMask { blindData[i] ^= blindMask[i] } blind = blindData balance := make([]byte, len(coinBalanceData)) for i := range coinBalanceData { balance[len(balance)-i-1] = coinBalanceData[i] ^ coinMask[i] coinBalanceData[i] ^= coinMask[i] } // If non-divisible, outputs should be aligned input-relative if tx.config.Behavior&Divisible == 0 { addRef, err := tree.Get([]byte{byte(addRef1Index)}) if err != nil { return nil, errors.Wrap(err, "prove input") } addRefKey, err := tree.Get([]byte{byte(addRef2Index)}) if err != nil { return nil, errors.Wrap(err, "prove input") } tx.Outputs[index].rawAdditionalReference = addRef tx.Outputs[index].rawAdditionalReferenceKey = addRefKey } i.value = new(big.Int).SetBytes(balance) i.signOp = func(transcript []byte) error { i.Signature = tx.bulletproofProver.SignHidden( shared.Private(), spendKey.Private(), transcript, coinBalanceData, blindData, ) return nil } if tx.rdfMultiprover == nil || tx.rdfHypergraphSchema == "" { return nil, errors.Wrap( errors.New("RDF multiprover not available"), "prove input", ) } if tx.config.Behavior&Acceptable == 0 { // coin:Coin inputs fields := []string{ "coin:Coin.Commitment", "coin:Coin.VerificationKey", } if tx.config.Behavior&Divisible == 0 { fields = append( fields, "coin:Coin.AdditionalReference", "coin:Coin.AdditionalReferenceKey", ) } typeIndex := uint64(63) multiproof, err := tx.rdfMultiprover.ProveWithType( tx.rdfHypergraphSchema, fields, tree, &typeIndex, ) if err != nil { return nil, errors.Wrap(err, "prove input") } multiproofBytes, err := multiproof.ToBytes() if err != nil { return nil, errors.Wrap(err, "prove input") } i.Proofs = [][]byte{multiproofBytes} if tx.config.Behavior&Divisible == 0 { addref, err := tx.rdfMultiprover.Get( tx.rdfHypergraphSchema, "coin:Coin", "AdditionalReference", tree, ) if err != nil { return nil, errors.Wrap(err, "prove input") } addrefkey, err := tx.rdfMultiprover.Get( tx.rdfHypergraphSchema, "coin:Coin", "AdditionalReferenceKey", tree, ) if err != nil { return nil, errors.Wrap(err, "prove input") } i.Proofs = append(i.Proofs, slices.Concat(addref, addrefkey)) } } else { // pending:PendingTransaction inputs fields := []string{ "pending:PendingTransaction.Commitment", "pending:PendingTransaction.ToVerificationKey", "pending:PendingTransaction.RefundVerificationKey", } if tx.config.Behavior&Divisible == 0 { fields = append( fields, "pending:PendingTransaction.ToAdditionalReference", "pending:PendingTransaction.ToAdditionalReferenceKey", "pending:PendingTransaction.RefundAdditionalReference", "pending:PendingTransaction.RefundAdditionalReferenceKey", ) } // Add expiration field if needed if tx.config.Behavior&Expirable != 0 { fields = append(fields, "pending:PendingTransaction.Expiration") } typeIndex := uint64(63) multiproof, err := tx.rdfMultiprover.ProveWithType( tx.rdfHypergraphSchema, fields, tree, &typeIndex, ) if err != nil { return nil, errors.Wrap(err, "prove input") } multiproofBytes, err := multiproof.ToBytes() if err != nil { return nil, errors.Wrap(err, "prove input") } i.Proofs = [][]byte{multiproofBytes} // Get expiration value if needed var exp []byte = nil if tx.config.Behavior&Expirable != 0 { exp, err = tx.rdfMultiprover.Get( tx.rdfHypergraphSchema, "pending:PendingTransaction", "Expiration", tree, ) if err != nil { return nil, errors.Wrap(err, "prove input") } } if exp != nil { i.Proofs = append(i.Proofs, exp) } // To, so refund if keyImageIndex>>2 == 4 { refundImage, err := tree.Get([]byte{5 << 2}) if err != nil { return nil, errors.Wrap(err, "prove input") } i.Proofs = append(i.Proofs, []byte{2}, refundImage) } else { // Refund, so to toImage, err := tree.Get([]byte{4 << 2}) if err != nil { return nil, errors.Wrap(err, "prove input") } i.Proofs = append(i.Proofs, []byte{1}, toImage) } if tx.config.Behavior&Divisible == 0 { addref, err := tx.rdfMultiprover.Get( tx.rdfHypergraphSchema, "pending:PendingTransaction", "ToAdditionalReference", tree, ) if err != nil { return nil, errors.Wrap(err, "prove input") } addrefkey, err := tx.rdfMultiprover.Get( tx.rdfHypergraphSchema, "pending:PendingTransaction", "ToAdditionalReferenceKey", tree, ) if err != nil { return nil, errors.Wrap(err, "prove input") } i.Proofs = append(i.Proofs, slices.Concat(addref, addrefkey)) } } i.Commitment = commitment return blind, nil } // Verifies an input's signature, if a QUIL transaction, allows legacy // verification. If invalid, has an associated error. func (i *TransactionInput) Verify( frameNumber uint64, tx *Transaction, transcript []byte, checkLegacy bool, txMultiproof *qcrypto.TraversalProof, index int, ) (bool, error) { if len(i.Commitment) != 56 { return false, errors.Wrap( errors.New("invalid commitment length"), "verify input", ) } if len(i.Signature) != 336 { return false, errors.Wrap( errors.New("invalid signature length"), "verify input", ) } if !bytes.Equal(i.Commitment, i.Signature[(56*5):(56*6)]) { return false, errors.Wrap( errors.New("invalid commitment"), "verify input", ) } spendCheckBI, err := poseidon.HashBytes(i.Signature[56*4 : 56*5]) if err != nil { return false, errors.Wrap( errors.New("invalid address"), "verify input", ) } // Key has been committed to hypergraph, Coin is already spent if v, err := tx.hypergraph.GetVertex([64]byte(slices.Concat( tx.Domain[:], spendCheckBI.FillBytes(make([]byte, 32)), ))); err == nil && v != nil { return false, errors.Wrap(err, "verify input") } addRefDelta := 0 if tx.config.Behavior&Divisible == 0 { addRefDelta++ if len(i.Proofs[len(i.Proofs)-1]) != 64+56 { return false, errors.Wrap( errors.New("invalid proof"), "verify input", ) } } if len(i.Proofs) == 1+addRefDelta { if tx.config.Behavior&Acceptable != 0 { return false, errors.Wrap( errors.New(fmt.Sprintf("invalid proof length: %d", len(i.Proofs))), "verify input", ) } coinTypeBI, err := poseidon.HashBytes( slices.Concat(tx.Domain[:], []byte("coin:Coin")), ) if err != nil { return false, errors.Wrap(err, "verify input") } inputs := [][]byte{ i.Signature[56*5 : 56*6], i.Signature[56*4 : 56*5], } indices := []int{1, 3} if tx.config.Behavior&Divisible == 0 { inputs = append(inputs, i.Proofs[1][:64], i.Proofs[1][64:]) indices = append(indices, 6, 7) } indices = append(indices, 63) coinTypeBytes := coinTypeBI.FillBytes(make([]byte, 32)) inputs = append(inputs, coinTypeBytes) keys := make([][]byte, len(inputs)) keys[len(keys)-1] = bytes.Repeat([]byte{0xff}, 32) if valid, err := i.verifyProof( tx.hypergraph, inputs, i.Proofs[0], txMultiproof, indices, keys, index, ); err != nil || !valid { return false, err } } else { if tx.config.Behavior&Acceptable == 0 { return false, errors.Wrap( errors.New(fmt.Sprintf("invalid proof length: %d", len(i.Proofs))), "verify input", ) } pendingTypeBI, err := poseidon.HashBytes( slices.Concat(tx.Domain[:], []byte("pending:PendingTransaction")), ) if err != nil { return false, errors.Wrap(err, "verify input") } pendingTypeBytes := pendingTypeBI.FillBytes(make([]byte, 32)) indices := []int{1, 4, 5} data := [][]byte{i.Signature[56*5 : 56*6]} offset := 0 expiration := uint64(0) if tx.config.Behavior&Expirable != 0 { offset = 1 proofIndex := 10 if len(i.Proofs) != 4+addRefDelta { return false, errors.Wrap( errors.New(fmt.Sprintf("invalid proof length: %d", len(i.Proofs))), "verify input", ) } if tx.config.Behavior&Divisible == 0 { indices = append(indices, 10, 11, 12, 13) proofIndex = 14 } expiration = binary.BigEndian.Uint64(i.Proofs[1]) indices = append(indices, proofIndex) } else { if len(i.Proofs) != 3+addRefDelta { return false, errors.Wrap( errors.New(fmt.Sprintf("invalid proof length: %d", len(i.Proofs))), "verify input", ) } } altCheckBI, err := poseidon.HashBytes(i.Proofs[offset+2]) if err != nil { return false, errors.Wrap( errors.New("invalid address"), "verify input", ) } // Key has been committed to hypergraph, Pending is already spent if v, err := tx.hypergraph.GetVertex([64]byte(slices.Concat( tx.Domain[:], altCheckBI.FillBytes(make([]byte, 32)), ))); err == nil && v != nil { return false, errors.Wrap(err, "verify input") } isTo := bytes.Equal(i.Proofs[offset+1], []byte{2}) if isTo { data = append(data, i.Signature[56*4:56*5], i.Proofs[offset+2]) } else { if frameNumber < expiration { return false, errors.Wrap( errors.New("not expired"), "verify input", ) } data = append(data, i.Proofs[offset+2], i.Signature[56*4:56*5]) } if tx.config.Behavior&Divisible == 0 { data = append( data, i.Proofs[offset+3][:64], i.Proofs[offset+3][64:], i.Proofs[offset+3][:64], i.Proofs[offset+3][64:], ) } if tx.config.Behavior&Expirable != 0 { data = append(data, i.Proofs[1]) } data = append(data, pendingTypeBytes) indices = append(indices, 63) keys := [][]byte{} for i := 0; i < len(data)-1; i++ { keys = append(keys, nil) } keys = append(keys, bytes.Repeat([]byte{0xff}, 32)) if valid, err := i.verifyProof( tx.hypergraph, data, i.Proofs[0], txMultiproof, indices, keys, index, ); err != nil || !valid { return false, errors.Wrap(err, "verify input") } } return tx.bulletproofProver.VerifyHidden( i.Signature[(56*0):(56*1)], transcript, i.Signature[(56*1):(56*2)], i.Signature[(56*2):(56*3)], i.Signature[(56*3):(56*4)], i.Signature[(56*4):(56*5)], i.Signature[(56*5):(56*6)], ), nil } func (i *TransactionInput) verifyProof( hg hypergraph.Hypergraph, data [][]byte, proof []byte, txMultiproof *qcrypto.TraversalProof, indices []int, keys [][]byte, index int, ) (bool, error) { commits := [][]byte{} // same value, but needs to repeat evaluations := [][]byte{} uindices := []uint64{} for i, d := range data { h := sha512.New() h.Write([]byte{0}) if keys[i] == nil { h.Write([]byte{byte(indices[i]) << 2}) } else { h.Write(keys[i]) } h.Write(d) out := h.Sum(nil) evaluations = append(evaluations, out) commits = append( commits, txMultiproof.SubProofs[index].Ys[len(txMultiproof.SubProofs[index].Ys)-1], ) uindices = append(uindices, uint64(indices[i])) } mp := hg.GetProver().NewMultiproof() if err := mp.FromBytes(proof); err != nil { return false, errors.Wrap(err, "verify proof") } if valid := hg.GetProver().VerifyMultiple( commits, evaluations, uindices, 64, mp.GetMulticommitment(), mp.GetProof(), ); !valid { return false, errors.Wrap( errors.New("invalid proof"), "verify input", ) } return true, nil } // RecipientBundle is the bundle of values that represents a recipient's output. type RecipientBundle struct { // Public output values: // The public key used to derive shared secret for mask and balance. OneTimeKey []byte // Raw pubkey value is stored // The public key used to verify spend output. VerificationKey []byte // Raw pubkey value is stored // The encrypted underlying masked balance of the coin. CoinBalance []byte // The encrypted mask. Mask []byte // The address value optionally used for non-divisible/interchangeable units. AdditionalReference []byte // The key associated with the AdditionalReference, present if // AdditionalReference is present AdditionalReferenceKey []byte // Private output values use for construction of public values: // The public view key of the recipient recipientView []byte // The public spend key of the recipient recipientSpend []byte } // TransactionOutput is the output specific to a Transaction. When encoded as // the finalized state of the token intrinsic operation, produces a coin:Coin. type TransactionOutput struct { // Public output values: // The frame number this output is created on FrameNumber []byte // The commitment to the balance Commitment []byte // Raw commitment value is stored // The output entries corresponding to the serialized coin:Coin RecipientOutput RecipientBundle // Private output values use for construction of public values: // The underlying quantity used to generate the output value *big.Int // The underlying raw additionalReference value mapped from input, if present rawAdditionalReference []byte // The underlying raw additionalReferenceKey value mapped from input, if // present rawAdditionalReferenceKey []byte } func NewTransactionOutput( value *big.Int, recipientViewPubkey []byte, recipientSpendPubkey []byte, ) (*TransactionOutput, error) { return &TransactionOutput{ value: value, RecipientOutput: RecipientBundle{ recipientView: recipientViewPubkey, // buildutils:allow-slice-alias slice is static recipientSpend: recipientSpendPubkey, // buildutils:allow-slice-alias slice is static }, }, nil } func (o *TransactionOutput) Prove( res crypto.RangeProofResult, index int, tx *Transaction, frameNumber uint64, ) error { o.Commitment = res.Commitment[index*56 : (index+1)*56] blind := slices.Clone(res.Blinding[index*56 : (index+1)*56]) r, err := tx.decafConstructor.New() if err != nil { return errors.Wrap(err, "prove output") } shared, err := r.AgreeWithAndHashToScalar(o.RecipientOutput.recipientView) if err != nil { return errors.Wrap(err, "prove output") } blindMask := make([]byte, 56) coinMask := make([]byte, 56) shake := sha3.NewCShake256([]byte{}, []byte("blind")) shake.Write(shared.Public()) shake.Read(blindMask) shake = sha3.NewCShake256([]byte{}, []byte("coin")) shake.Write(shared.Public()) shake.Read(coinMask) for i := range blindMask { blind[i] ^= blindMask[i] } rawBalance := o.value.FillBytes(make([]byte, 56)) slices.Reverse(rawBalance) for i := range rawBalance { rawBalance[i] = rawBalance[i] ^ coinMask[i] } o.RecipientOutput.Mask = blind o.RecipientOutput.CoinBalance = rawBalance o.RecipientOutput.OneTimeKey = r.Public() o.RecipientOutput.VerificationKey, err = shared.Add( o.RecipientOutput.recipientSpend, ) if err != nil { return errors.Wrap(err, "prove output") } // TODO(2.1.1+): there's some other options we can pursue here, leaving this // section a bare copy until then. if tx.config.Behavior&Divisible == 0 { addRef := slices.Clone(tx.Outputs[index].rawAdditionalReference) addRefKey := slices.Clone(tx.Outputs[index].rawAdditionalReferenceKey) tx.Outputs[index].RecipientOutput.AdditionalReference = addRef tx.Outputs[index].RecipientOutput.AdditionalReferenceKey = addRefKey } o.FrameNumber = binary.BigEndian.AppendUint64(nil, frameNumber) return nil } func (o *TransactionOutput) Verify( frameNumber uint64, config *TokenIntrinsicConfiguration, ) (bool, error) { if frameNumber <= binary.BigEndian.Uint64(o.FrameNumber) { return false, errors.Wrap( errors.New("invalid frame number"), "verify output", ) } if len(o.Commitment) != 56 || len(o.RecipientOutput.VerificationKey) != 56 || len(o.RecipientOutput.CoinBalance) != 56 { return false, errors.Wrap( errors.New("invalid commitment, verification key, or coin balance"), "verify output", ) } if config.Behavior&Divisible == 0 { if len(o.RecipientOutput.AdditionalReference) != 64 { return false, errors.Wrap( errors.New("missing additional reference for non-divisible coin"), "verify output", ) } if len(o.RecipientOutput.AdditionalReferenceKey) != 56 { return false, errors.Wrap( errors.New("missing additional reference key for non-divisible coin"), "verify output", ) } } if len(o.RecipientOutput.Mask) != 56 { return false, errors.Wrap(errors.New("missing mask"), "verify output") } return true, nil } // Transaction defines the intrinsic execution for converting a collection of // coin:Coin or pending:PendingTransaction inputs into coin:Coin outputs, // depending on configuration. If the token has Acceptable flows enabled, // expects inputs of type pending:PendingTransaction in configuration, otherwise // expects coin:Coin inputs. type Transaction struct { Domain [32]byte Inputs []*TransactionInput Outputs []*TransactionOutput Fees []*big.Int RangeProof []byte TraversalProof *qcrypto.TraversalProof hypergraph hypergraph.Hypergraph bulletproofProver crypto.BulletproofProver inclusionProver crypto.InclusionProver verEnc crypto.VerifiableEncryptor decafConstructor crypto.DecafConstructor keyRing keys.KeyRing config *TokenIntrinsicConfiguration rdfHypergraphSchema string rdfMultiprover *schema.RDFMultiprover } func NewTransaction( domain [32]byte, inputs []*TransactionInput, outputs []*TransactionOutput, fees []*big.Int, config *TokenIntrinsicConfiguration, hypergraph hypergraph.Hypergraph, bulletproofProver crypto.BulletproofProver, inclusionProver crypto.InclusionProver, verEnc crypto.VerifiableEncryptor, decafConstructor crypto.DecafConstructor, keyRing keys.KeyRing, rdfHypergraphSchema string, rdfMultiprover *schema.RDFMultiprover, ) *Transaction { return &Transaction{ Domain: domain, Inputs: inputs, // buildutils:allow-slice-alias slice is static Outputs: outputs, // buildutils:allow-slice-alias slice is static Fees: fees, // buildutils:allow-slice-alias slice is static hypergraph: hypergraph, bulletproofProver: bulletproofProver, inclusionProver: inclusionProver, verEnc: verEnc, decafConstructor: decafConstructor, keyRing: keyRing, config: config, rdfHypergraphSchema: rdfHypergraphSchema, rdfMultiprover: rdfMultiprover, } } // GetCost implements intrinsics.IntrinsicOperation. func (tx *Transaction) GetCost() (*big.Int, error) { size := big.NewInt(int64(len(tx.Domain))) size.Add(size, big.NewInt(int64(len(tx.RangeProof)))) pb, err := tx.TraversalProof.ToBytes() if err != nil { return nil, errors.Wrap(err, "get cost") } size.Add(size, big.NewInt(int64(len(pb)))) for _, o := range tx.Outputs { size.Add(size, big.NewInt(8)) // frame number size.Add(size, big.NewInt(int64(len(o.Commitment)))) size.Add(size, big.NewInt(int64(len(o.RecipientOutput.CoinBalance)))) size.Add(size, big.NewInt(int64(len(o.RecipientOutput.Mask)))) size.Add(size, big.NewInt(int64(len(o.RecipientOutput.OneTimeKey)))) size.Add(size, big.NewInt(int64(len(o.RecipientOutput.VerificationKey)))) if len(o.RecipientOutput.AdditionalReference) == 64 { size.Add(size, big.NewInt(120)) } } return size, nil } // Materialize implements intrinsics.IntrinsicOperation. func (tx *Transaction) Materialize( frameNumber uint64, state state.State, ) (state.State, error) { ms := state.(*hgstate.HypergraphState) // Create the coin type hash coinTypeBI, err := poseidon.HashBytes( slices.Concat(tx.Domain[:], []byte("coin:Coin")), ) if err != nil { return nil, errors.Wrap(err, "materialize") } coinTypeBytes := coinTypeBI.FillBytes(make([]byte, 32)) // For each output, create a coin for _, output := range tx.Outputs { // Create coin tree coinTree := &qcrypto.VectorCommitmentTree{} // Index 0: FrameNumber if err := coinTree.Insert( []byte{0}, output.FrameNumber, nil, big.NewInt(8), ); err != nil { return nil, errors.Wrap(err, "materialize") } // Index 1: Commitment if err := coinTree.Insert( []byte{1 << 2}, output.Commitment, nil, big.NewInt(56), ); err != nil { return nil, errors.Wrap(err, "materialize") } // Index 2: OneTimeKey if err := coinTree.Insert( []byte{2 << 2}, output.RecipientOutput.OneTimeKey, nil, big.NewInt(56), ); err != nil { return nil, errors.Wrap(err, "materialize") } // Index 3: VerificationKey if err := coinTree.Insert( []byte{3 << 2}, output.RecipientOutput.VerificationKey, nil, big.NewInt(56), ); err != nil { return nil, errors.Wrap(err, "materialize") } // Index 4: CoinBalance (encrypted) if err := coinTree.Insert( []byte{4 << 2}, output.RecipientOutput.CoinBalance, nil, big.NewInt(56), ); err != nil { return nil, errors.Wrap(err, "materialize") } // Index 5: Mask (encrypted) if err := coinTree.Insert( []byte{5 << 2}, output.RecipientOutput.Mask, nil, big.NewInt(56), ); err != nil { return nil, errors.Wrap(err, "materialize") } // Index 6 & 7: Additional references (for non-divisible tokens) if len(output.RecipientOutput.AdditionalReference) == 64 && len(output.RecipientOutput.AdditionalReferenceKey) == 56 { if err := coinTree.Insert( []byte{6 << 2}, output.RecipientOutput.AdditionalReference, nil, big.NewInt(56), ); err != nil { return nil, errors.Wrap(err, "materialize") } if err := coinTree.Insert( []byte{7 << 2}, output.RecipientOutput.AdditionalReferenceKey, nil, big.NewInt(56), ); err != nil { return nil, errors.Wrap(err, "materialize") } } // Type marker at max index if err := coinTree.Insert( bytes.Repeat([]byte{0xff}, 32), coinTypeBytes, nil, big.NewInt(32), ); err != nil { return nil, errors.Wrap(err, "materialize") } // Compute address and add to state commit := coinTree.Commit(tx.inclusionProver, false) outAddrBI, err := poseidon.HashBytes(commit) if err != nil { return nil, errors.Wrap(err, "materialize") } coinAddress := outAddrBI.FillBytes(make([]byte, 32)) // Create materialized state for coin coinState := ms.NewVertexAddMaterializedState( tx.Domain, [32]byte(coinAddress), frameNumber, nil, coinTree, ) // Set the state err = ms.Set( tx.Domain[:], coinAddress, hgstate.VertexAddsDiscriminator, frameNumber, coinState, ) if err != nil { return nil, errors.Wrap(err, "materialize") } } // Mark inputs as spent for _, input := range tx.Inputs { if len(input.Signature) == 336 { // Standard format verificationKey := input.Signature[56*4 : 56*5] spendCheckBI, err := poseidon.HashBytes(verificationKey) if err != nil { return nil, errors.Wrap(err, "materialize") } // Create spent marker spentTree := &qcrypto.VectorCommitmentTree{} if err := spentTree.Insert( []byte{0}, []byte{0x01}, nil, big.NewInt(0), ); err != nil { return nil, errors.Wrap(err, "materialize") } spentAddress := spendCheckBI.FillBytes(make([]byte, 32)) // Create materialized state for spend spentState := ms.NewVertexAddMaterializedState( tx.Domain, [32]byte(spentAddress), frameNumber, nil, spentTree, ) // Set the state err = ms.Set( tx.Domain[:], spentAddress, hgstate.VertexAddsDiscriminator, frameNumber, spentState, ) if err != nil { return nil, errors.Wrap(err, "materialize") } } } return ms, nil } func (tx *Transaction) Prove(frameNumber uint64) error { if len(tx.Inputs) == 0 || len(tx.Outputs) == 0 || len(tx.Inputs) > 100 || len(tx.Outputs) > 100 { return errors.Wrap( errors.New("invalid quantity of inputs, outputs, or proofs"), "prove", ) } values := []*big.Int{} for _, o := range tx.Outputs { values = append(values, o.value) } addresses := [][]byte{} blinds := []byte{} for i, input := range tx.Inputs { blind, err := input.Prove(tx, i) if err != nil { return errors.Wrap(err, "prove") } addresses = append(addresses, input.address) blinds = append(blinds, blind...) } res, err := tx.bulletproofProver.GenerateRangeProofFromBig( values, blinds, 128, ) if err != nil { return err } if len(res.Commitment) != len(tx.Outputs)*56 || len(res.Blinding) != len(tx.Outputs)*56 { return errors.Wrap(errors.New("invalid range proof"), "prove") } for i, o := range tx.Outputs { if err := o.Prove(res, i, tx, frameNumber); err != nil { return errors.Wrap(err, "prove") } } challenge, err := tx.GetChallenge() if err != nil { return errors.Wrap(err, "prove") } for _, input := range tx.Inputs { if err := input.signOp(challenge); err != nil { return errors.Wrap(err, "prove") } } tx.RangeProof = res.Proof tx.TraversalProof, err = tx.hypergraph.CreateTraversalProof( tx.Domain, hypergraph.VertexAtomType, hypergraph.AddsPhaseType, addresses, ) if err != nil { return errors.Wrap(err, "prove") } return nil } func (tx *Transaction) GetReadAddresses( frameNumber uint64, ) ([][]byte, error) { return nil, nil } func (tx *Transaction) GetWriteAddresses( frameNumber uint64, ) ([][]byte, error) { // Create the coin type hash coinTypeBI, err := poseidon.HashBytes( slices.Concat(tx.Domain[:], []byte("coin:Coin")), ) if err != nil { return nil, errors.Wrap(err, "get write addresses") } coinTypeBytes := coinTypeBI.FillBytes(make([]byte, 32)) addresses := [][]byte{} // For each output, create a coin for _, output := range tx.Outputs { // Create coin tree coinTree := &qcrypto.VectorCommitmentTree{} // Index 0: FrameNumber if err := coinTree.Insert( []byte{0}, output.FrameNumber, nil, big.NewInt(8), ); err != nil { return nil, errors.Wrap(err, "get write addresses") } // Index 1: Commitment if err := coinTree.Insert( []byte{1 << 2}, output.Commitment, nil, big.NewInt(56), ); err != nil { return nil, errors.Wrap(err, "get write addresses") } // Index 2: OneTimeKey if err := coinTree.Insert( []byte{2 << 2}, output.RecipientOutput.OneTimeKey, nil, big.NewInt(56), ); err != nil { return nil, errors.Wrap(err, "get write addresses") } // Index 3: VerificationKey if err := coinTree.Insert( []byte{3 << 2}, output.RecipientOutput.VerificationKey, nil, big.NewInt(56), ); err != nil { return nil, errors.Wrap(err, "get write addresses") } // Index 4: CoinBalance (encrypted) if err := coinTree.Insert( []byte{4 << 2}, output.RecipientOutput.CoinBalance, nil, big.NewInt(56), ); err != nil { return nil, errors.Wrap(err, "get write addresses") } // Index 5: Mask (encrypted) if err := coinTree.Insert( []byte{5 << 2}, output.RecipientOutput.Mask, nil, big.NewInt(56), ); err != nil { return nil, errors.Wrap(err, "get write addresses") } // Index 6 & 7: Additional references (for non-divisible tokens) if len(output.RecipientOutput.AdditionalReference) == 64 && len(output.RecipientOutput.AdditionalReferenceKey) == 56 { if err := coinTree.Insert( []byte{6 << 2}, output.RecipientOutput.AdditionalReference, nil, big.NewInt(56), ); err != nil { return nil, errors.Wrap(err, "get write addresses") } if err := coinTree.Insert( []byte{7 << 2}, output.RecipientOutput.AdditionalReferenceKey, nil, big.NewInt(56), ); err != nil { return nil, errors.Wrap(err, "get write addresses") } } // Type marker at max index if err := coinTree.Insert( bytes.Repeat([]byte{0xff}, 32), coinTypeBytes, nil, big.NewInt(32), ); err != nil { return nil, errors.Wrap(err, "get write addresses") } // Compute address and add to state commit := coinTree.Commit(tx.inclusionProver, false) outAddrBI, err := poseidon.HashBytes(commit) if err != nil { return nil, errors.Wrap(err, "get write addresses") } coinAddress := outAddrBI.FillBytes(make([]byte, 32)) addresses = append(addresses, slices.Concat( tx.Domain[:], coinAddress, )) } // Mark inputs as spent for _, input := range tx.Inputs { if len(input.Signature) == 336 { // Standard format verificationKey := input.Signature[56*4 : 56*5] spendCheckBI, err := poseidon.HashBytes(verificationKey) if err != nil { return nil, errors.Wrap(err, "get write addresses") } // Create spent marker spentTree := &qcrypto.VectorCommitmentTree{} if err := spentTree.Insert( []byte{0}, []byte{0x01}, nil, big.NewInt(0), ); err != nil { return nil, errors.Wrap(err, "get write addresses") } spentAddress := spendCheckBI.FillBytes(make([]byte, 32)) addresses = append(addresses, slices.Concat( tx.Domain[:], spentAddress, )) } } return addresses, nil } func (tx *Transaction) GetChallenge() ([]byte, error) { transcript := []byte{} transcript = append(transcript, tx.Domain[:]...) for _, o := range tx.Outputs { transcript = append(transcript, o.Commitment...) transcript = append(transcript, o.FrameNumber...) transcript = append(transcript, o.RecipientOutput.CoinBalance...) transcript = append(transcript, o.RecipientOutput.Mask...) transcript = append(transcript, o.RecipientOutput.OneTimeKey...) transcript = append(transcript, o.RecipientOutput.VerificationKey...) if len(o.RecipientOutput.AdditionalReference) == 64 { transcript = append(transcript, o.RecipientOutput.AdditionalReference...) transcript = append( transcript, o.RecipientOutput.AdditionalReferenceKey..., ) } } challenge, err := tx.decafConstructor.HashToScalar(transcript) return challenge.Private(), errors.Wrap(err, "get challenge") } // Verifies the transaction's validity at the given frame number. If invalid, // also provides the associated error. func (tx *Transaction) Verify(frameNumber uint64) (bool, error) { if len(tx.Inputs) == 0 || len(tx.Outputs) == 0 || len(tx.Inputs) > 100 || len(tx.Outputs) > 100 || len(tx.Inputs) != len(tx.TraversalProof.SubProofs) { return false, errors.Wrap( errors.New("invalid quantity of inputs, outputs, or proofs"), "verify: invalid transaction", ) } for _, fee := range tx.Fees { if fee == nil || new(big.Int).Lsh(big.NewInt(1), uint(128)).Cmp(fee) < 0 || new(big.Int).Cmp(fee) > 0 { return false, errors.Wrap(errors.New("invalid fees"), "verify: invalid transaction") } } if tx.config.Behavior&Divisible == 0 && len(tx.Inputs) != len(tx.Outputs) { return false, errors.Wrap( errors.New("non-divisible token has mismatching inputs and outputs"), "verify: invalid transaction", ) } challenge, err := tx.GetChallenge() if err != nil { return false, errors.Wrap(err, "verify: invalid transaction") } inputs := [][]byte{} check := map[string]struct{}{} for i, input := range tx.Inputs { if valid, err := input.Verify( frameNumber, tx, challenge, bytes.Equal(tx.Domain[:], QUIL_TOKEN_ADDRESS), tx.TraversalProof, i, ); !valid { return false, errors.Wrap(err, "verify: invalid transaction") } if _, ok := check[string(input.Signature[(56*4):(56*5)])]; ok { return false, errors.Wrap( errors.New("attempted double-spend"), "verify: invalid transaction", ) } check[string(input.Signature[(56*4):(56*5)])] = struct{}{} inputs = append(inputs, input.Commitment) } commitment := make([]byte, len(tx.Outputs)*56) commitments := [][]byte{} for i, o := range tx.Outputs { if valid, err := o.Verify(frameNumber, tx.config); !valid { return false, errors.Wrap(err, "verify: invalid transaction") } if tx.config.Behavior&Divisible == 0 { if !bytes.Equal( o.RecipientOutput.AdditionalReference, tx.Inputs[i].Proofs[len(tx.Inputs[i].Proofs)-1][:64], ) || !bytes.Equal( o.RecipientOutput.AdditionalReferenceKey, tx.Inputs[i].Proofs[len(tx.Inputs[i].Proofs)-1][64:], ) { return false, errors.Wrap(errors.New("invalid reference"), "verify: invalid transaction") } } spendCheckBI, err := poseidon.HashBytes(o.RecipientOutput.VerificationKey) if err != nil { return false, errors.Wrap(err, "verify: invalid transaction") } _, err = tx.hypergraph.GetVertex([64]byte( slices.Concat(tx.Domain[:], spendCheckBI.FillBytes(make([]byte, 32))), )) if err == nil { return false, errors.Wrap( errors.New("invalid verification key"), "verify: invalid transaction", ) } copy(commitment[i*56:(i+1)*56], tx.Outputs[i].Commitment[:]) commitments = append(commitments, tx.Outputs[i].Commitment) } roots, err := tx.hypergraph.GetShardCommits( binary.BigEndian.Uint64(tx.Outputs[0].FrameNumber), tx.Domain[:], ) if err != nil { return false, errors.Wrap(err, "verify: invalid transaction") } valid, err := tx.hypergraph.VerifyTraversalProof( tx.Domain, hypergraph.VertexAtomType, hypergraph.AddsPhaseType, roots[0], tx.TraversalProof, ) if err != nil || !valid { return false, errors.Wrap(err, "verify: invalid transaction") } if !tx.bulletproofProver.VerifyRangeProof(tx.RangeProof, commitment, 128) { return false, errors.Wrap(errors.New("invalid range proof"), "verify: invalid transaction") } sumcheckFees := []*big.Int{} if bytes.Equal(tx.Domain[:], QUIL_TOKEN_ADDRESS) { sumcheckFees = append(sumcheckFees, tx.Fees...) } if !tx.bulletproofProver.SumCheck( inputs, []*big.Int{}, commitments, sumcheckFees, ) { return false, errors.Wrap(errors.New("invalid sum check"), "verify: invalid transaction") } return true, nil } var _ intrinsics.IntrinsicOperation = (*Transaction)(nil)