Skip to content

test(zkatsnark): add regression tests and fix transfer action validation - #2373

Open
neetance wants to merge 1 commit into
LFDT-Panurus:feature/zkat-snark-driverfrom
neetance:test/zkatsnark-regression
Open

neetance wants to merge 1 commit into
LFDT-Panurus:feature/zkat-snark-driverfrom
neetance:test/zkatsnark-regression

Conversation

@neetance

Copy link
Copy Markdown
Contributor

Summary

This PR adds an end-to-end regression test suite for the zkatsnark token driver and resolves key transfer action validation and signature verification requirements:

  1. End-to-End Regression Test Suite (regression_test.go):
    • Tests complete token lifecycle flows (Issue and Transfer) end-to-end using real SNARK provers (SpendProver, OutputProver), circuits, and validators.
    • Verifies single-input and multi-input transfer requests, ensuring ledger state resolution, SNARK proof verification, and signature consumption behave as expected.
  2. Transfer Signature Validation & Consumption (validator_transfer_sig.go):
    • Implemented TransferSignatureValidate to satisfy the common token framework validation contract.
    • Validates that each spend input is signed by its respective owner identity and that signatures are properly consumed from the signature provider (EnsureExhausted).
  3. Input Token State Matching in Transfer Actions:
    • Updated TransferAction to hold and serialize original input OutputDescriptions in InputTokens, ensuring ledger lookup and token key verification succeed during transfer validation.

Testing

All tests pass

 go test ./...
?       github.com/LFDT-Panurus/panurus/x/token/core/zkatsnark  [no test files]
ok      github.com/LFDT-Panurus/panurus/x/token/core/zkatsnark/circuit  148.514s
ok      github.com/LFDT-Panurus/panurus/x/token/core/zkatsnark/circuit/gadgets  0.043s
ok      github.com/LFDT-Panurus/panurus/x/token/core/zkatsnark/crypto/jubjub    0.121s
ok      github.com/LFDT-Panurus/panurus/x/token/core/zkatsnark/crypto/mimc      0.045s
?       github.com/LFDT-Panurus/panurus/x/token/core/zkatsnark/crypto/params    [no test files]
?       github.com/LFDT-Panurus/panurus/x/token/core/zkatsnark/driver   [no test files]
ok      github.com/LFDT-Panurus/panurus/x/token/core/zkatsnark/pp       0.039s
ok      github.com/LFDT-Panurus/panurus/x/token/core/zkatsnark/prover   152.515s
ok      github.com/LFDT-Panurus/panurus/x/token/core/zkatsnark/regression       7.358s
ok      github.com/LFDT-Panurus/panurus/x/token/core/zkatsnark/setup    155.601s
ok      github.com/LFDT-Panurus/panurus/x/token/core/zkatsnark/token    0.044s
ok      github.com/LFDT-Panurus/panurus/x/token/core/zkatsnark/validator        183.331s

Signed-off-by: Ankit Basu <ankitbasu14@gmail.com>
var signatures [][]byte
var inputTokens []*snarktoken.Input

for i, inputID := range inputIDs {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation chain (TransferSignatureValidate + TransferZKValidate) never checks that the SNARK-proven Inputs[i].CommitmentIn corresponds to the token identified by the new InputIDs[i]/InputTokens[i] fields. Authorization and ledger deletion are decoupled from what the proof actually spends.

Failure scenario: An attacker owning tokens A (value 100) and B (value 1) builds a ZK-valid transfer whose SpendDescription proves the opening of A, but sets InputIDs/InputTokens to reference B. TransferSignatureValidate loads B from the ledger, finds the attacker as owner, and accepts the signature; TransferZKValidate verifies the (valid) proof over A's commitment; the RWS translator deletes B (value 1). Value conservation is enforced only over A (value 100), so the attacker mints ~99 units. Neither validator checks that Inputs[i].CommitmentIn == OutputDescription(InputTokens[i]).CommitmentOut


for i, inputID := range inputIDs {
// Load the on-chain output (OutputDescription) to get the owner
raw, err := ctx.Ledger.GetState(*inputID)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A crafted TokenRequest with a transfer action JSON

{"InputIDs":[null],"Inputs":[{...one spend...}]}

deserializes to a []*token2.ID slice containing a nil pointer.
The len(inputIDs)==len(action.Inputs) guard passes (both 1), then ctx.Ledger.GetState(*inputID) dereferences the nil pointer and panics, crashing the validating node on untrusted input.

action.InputIDs = tokenIDs
var inputTokens [][]byte
for _, loadedToken := range loadedTokens {
inputTokens = append(inputTokens, loadedToken.Token)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: append in a loop -> best to pre-allocate and index assign
Something like this:

inputTokens := make([][]byte, len(loadedTokens))
for i, token := range loadedTokens {
    inputTokens[i] = token.Token
}

}

var outputDesc snarktoken.OutputDescription
if err := json.Unmarshal(raw, &outputDesc); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds new fields to TransferAction.Deserialize (InputIDs/InputTokens/Issuer) and adds a validator that json.Unmarshals ledger tokens, but adds no fuzz target and no {name,pkg,func} entry to .github/workflows/nightly-fuzz.yml
The new parsing surface is never exercised under -fuzztime.

return a.InputIDs
}

// GetSerializedInputs returns the serialized inputs of the action.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetSerializedInputs falls back to serializing SpendDescriptions when InputTokens is empty, producing OutputSN keys that can never match the on-ledger token keys.

Failure scenario: If a TransferAction has InputIDs set but InputTokens empty (an action built/reconstructed without the new population step, or a wire action omitting InputTokens), GetSerializedInputs returns marshaled SpendDescriptions. translator.checkInputs then computes CreateOutputSNKey(TxId, Index, <SpendDescription bytes>), which never equals the key stored from the token's OutputDescription, so StateMustExist fails with 'input must exist'...
A hard-to-diagnose failure driven by which field happens to be populated.

transferRaw, err := transferAction.Serialize()
require.NoError(t, err)

tr := &driver.TokenRequest{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signatures here are never used.
so the test does not exercise the common framework's signature-scoping/EnsureExhausted path it claims to regression-test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants